Compiler Construction Notes
Compiler Construction Notes
D Ruikar
COMPILER CONSTRUCTION
--------------------------------------------------------------------------------------------------------------------------
SECTION - I
1. Introduction to Compiling: (3)
Compilers, Phases of a compiler, Compiler construction tools, and a simple one pass compiler.
SECTION - II
5. Run Time Environments: (3)
Source language issues, storage organization and allocation strategies, parameter passing,
symbol table organizations and generations, dynamic storage allocations.
Text Book:
1. Compilers - Principles, Techniques and Tools - A.V. Aho, R. Shethi and J.D. Ullman(Pearson
Education.)
References: -
1. Compiler Construction - Dhamdere (Mc-Millan)
2. Compiler Construction – Principles & Practice – Ken Louden ( Cengage Learning)
3. Compiler Design in C – Allen I. Holub (PHI / Pearson Education)
4. Compiler Construction - Barret, Bates, Couch (Galgotia)
5. Unix Programming - Pepkin Pike.
6. Crafting a compiler with C – Charls Fischer, Richard LeBlane (Pearson Education)
1
1. INTRODUCTION TO COMPILING
If the target program is an executable machine-language program, it can then be called by the
user to process inputs and produce outputs
Input Output
Target Program
Running the target program
Execute Instruction
An interpreter An interpreter follows Interpretation cycle
Compiler Interpreter
1 Translates Complete program at once translates program statement by statement
2 list of errors at a time if any only one error at a time if any
3 output if compilation is stored in Object File
output of interpretation is will not get stored
any where
4 only before first execution need to compile prior to each execution need to interpret the
the program after first execution same program
executable file can be used to execute the
program again
5 compilation is faster process interpretation is slower process
6 error diagnostics is poor than interpreter error diagnostics is better than compiler
2
Translation process
An executable target program, as shown in above Figure source program may be divided
into modules stored in separate files. The task of collecting the source program is sometimes
entrusted to a separate program, called a preprocessor.
The preprocessor may also expand shorthand’s, called macros, into source language
statements.
The modified source program is then fed to a compiler. The compiler may produce an
assembly-language program as its output, because assembly language is easier to produce as
output and is easier to debug.
The assembly language is then processed by a program called an assembler that produces
re-locatable machine code as its output.
Large programs are often compiled in pieces, so the re-locatable machine code may have
to be linked together with other re-locatable object files and library files into the code that
actually runs on the machine. The linker resolves external memory addresses, where the code in
one file may refer to a location in another file.
The loader then puts together the entire executable object files into memory for execution.
C program
Preprocessor
Modified C program
Compiler
Incomplete assembly language program
Assembler
Pre complied Re-Locatable incomplete machine code
library Linker
function
Target Program
The Structure of a Compiler
Up to this point we have treated a compiler as a single box that maps a source program
into a semantically equivalent target program. If we open up this box a little, we see that there
are two parts to this mapping: analysis and synthesis.
The analysis part breaks up the source program into constituent pieces and imposes a
grammatical structure on them. It then uses this structure to create an intermediate
representation of the source program. If the analysis part detects that the source program is
either syntactically ill formed or semantically unsound, then it must provide informative
messages, so the user can take corrective action. The analysis part also collects information about
the source program and stores it in a data structure called a symbol table, which is passed along
with the intermediate representation to the synthesis part.
The synthesis part constructs the desired target program from the intermediate
representation and the information in the symbol table.
Analysis is called as front end of compiler and synthesis is called as back end of compiler
Compilation process can be considered as a series of sub processes called as phases.
3
A phase is sub process, which takes some form of the source program as input and generates
another representation as an output
Phases of a compiler
Character stream
Lexical analysis
Tokens
Syntax analysis
Syntax tree
Semantic analysis
Intermediate representation
Cede generation
Target code
4
<id,1> <=> <id,2> <+> <id,3> <*> <60.00>
Syntax Analyzer
=
<id,1> +
<id,2> *
<id,3> 60.00
Semantic Analyzer
=
<id,1> +
<id,2> *
<id,3> inttofloat
Symbol table
60.00
id
Intermediate Code Generator
1 Position t1 = inttofloat(60.00)
2 initial t2 = <id,3> * t1
t3 = <id,2> + t2
3 Rate <id,1> = t3
Machine-Independent code optimizer
t1 = <id,3> * 60.00
<id,1> = <id,2> + t1
Code Generator
LDF AX, <id,3>
MULTF AX, #60.00
LDF BX, <id,2>
ADDF AX,BX
STF <id,1>
1. Lexical Analysis
The first phase of a compiler is called lexical analysis or scanning.
The lexical analyzer reads the stream of characters making up the source program and
groups the characters into meaningful sequences called lexemes.
For each lexeme, the lexical analyzer produces as output a token of the form
<token-name, attribute-value>
2. Syntax Analysis
The second phase of the compiler is syntax analysis or parsing. The parser uses the first
components of the tokens produced by the lexical analyzer to create a tree-like intermediate
representation that depicts the grammatical structure of the token stream.
5
A typical representation is a syntax tree in which each interior node represents an
operation and the children of the node represent the arguments of the operation.
3. Semantic Analysis
The semantic analyzer uses the syntax tree and the information in the symbol table to
check the source program for semantic consistency with the language definition.
An important part of semantic analysis is type checking, where the compiler checks that each
operator has matching operands.
For example, many programming language definitions require an array index to be an
integer; the compiler must report an error if a floating-point number is used to index an array.
The language specification may permit some type conversions called coercions.
For example, a binary arithmetic operator may be applied to either a pair of integers or to a
pair of floating-point numbers. If the operator is applied to a floating-point number and an
integer, the compiler may convert or coerce the integer into a floating-point number.
5. Code Generation
The code generator takes as input an intermediate representation of the source program
and maps it into the target language. If the target language is machine code, registers or memory
locations are selected for each of the variables used by the program. Then, the intermediate
instructions are translated into sequences of machine instructions that perform the same task. A
crucial aspect of code generation is the judicious assignment of registers to hold variables.
The intermediate code in might get translated into the machine code
1 LDF AX, id3 The first operand of each
2 MULF AX , #60.0 instruction specifies a destination. The F
3 LDF BX , id2 in each instruction tells us that it deals
4 ADDF AX , BX
with floating-point numbers.
5 STF idl
6. Code Optimization
The machine-independent code-optimization phase attempts to improve the intermediate
code so that better target code will result. Usually better means faster, but other objectives may
be desired, such as shorter code, or target code that consumes less power and memory.
Symbol-Table Management
An essential function of a compiler is to record the variable names used in the source
program and collect information about various attributes of each name.
These attributes may provide information
1) About the storage allocated for a name (on stack / static or dynamic data section)
2) Its data type,
6
3) Its scope (where in the program its value may be used),
In the case of procedure names
1. The number and types of its arguments, (parameters)
2. The method of passing each argument (for example, by value or by reference), and
3. The type returned.
The symbol table is a data structure containing a record for each variable name, with fields
for the attributes of the name
Symbol table also contains
Macro expansion table
Switch table
Compiler-Construction Tools
1. Parser generators
That automatically produces syntax analyzers from a grammatical description of a
programming language.
2. Scanner generators
That produces lexical analyzers from a regular-expression description of the tokens of a
language.
3. Syntax-directed translation engines
That produces collections of routines for walking a parse tree and generating intermediate code.
4. Code-generator generators
That produces a code generator from a collection of rules for translating each operation of the
intermediate language into the machine language for a target machine.
5. Data-flow analysis engines
That facilitate the gathering of information about how values are transmitted from one part of a
program to each other part. Data-flow analysis is a key part of code optimization.
6. Compiler-construction toolk2ts
That provides an integrated set of routines for constructing various phases of a compiler
7
While one-pass compilers may be faster than multi-pass compilers, they are unable to
generate as efficient programs, due to the limited scope available. (Many optimizations require
multiple passes over a program, subroutine, or basic block.) In addition, some programming
languages simply cannot be compiled in a single pass, as a result of their design.
In contrast, some programming languages have been designed specifically to be compiled
with one-pass compilers, and include special constructs to allow one-pass compilation. An
example of such a construct is the forward declaration in Pascal. Normally Pascal requires that
procedures be fully defined before use. This helps a one-pass compiler with its type checking:
calling a procedure that hasn't been defined is a clear error. However, this requirement makes
mutually recursive procedures impossible to implement.
If forward declaration is not made then compiler suffers from forward referencing
problem. Consider example in C language in main() function we give call to other user defined
function but the definition of those functions are usually implemented after main(). So the call to
function will suffers from forward referencing problem, function call will jump to such a much
memory location that is not yet defined. To deal with problem we need two pass compiler.
During first pass compiler generate stub instruction (stub instruction is such a jump
instruction which has label instead of address of memory location) and the table named
intermediate table in that compiler stores the labels and respective addresses when found.
Intermediate table is made to resolve forward referencing problem.
During second pass compiler traverse the code from beginning replaces each occurrence
of label by its corresponding address by looking in intermediate table this process is called as
back patching.
8
[Link] ANALYSIS
The Role of the Lexical Analyzer
As the first phase of a compiler, the main task of the lexical analyzer is to read the input
characters of the source program, group them into lexemes, and produce as output a sequence of
tokens for each lexeme in the source program.
The stream of tokens is sent to the parser for syntax analysis.
It is common for the lexical analyzer to interact with the symbol table as well. When the
lexical analyzer discovers a lexeme constituting an identifier, it needs to enter that lexeme into
the symbol table.
Token
Source Lexical Semantic
Parser
program Analysis analysis
GetNextToken
Symbol
Table
Interactions between the lexical analyzer and the parser
9
The token name is an abstract symbol representing a kind of lexical unit,
Token is smallest individual unit in program which has specific meaning
e.g., a particular keyword, a sequence of input characters denoting an identifier.
Pattern is a description of the form that the lexemes of a token may take.
For keyword as a token, the pattern is just the sequence of characters that form the keyword.
For identifiers and some other tokens, the pattern is a more complex structure that is matched by
many strings.
Generally patterns are Finite Automata or Regular Expression
Lexeme is a sequence of characters in the source program that matches the pattern for a
token and is identified by the lexical analyzer as an instance of that token
EXAMPLES OF TOKENS
1. Keyword: Keywords are reverse word whose meaning known to compiler in advance.
One token for each keyword. The pattern for a keyword is the same as the keyword itself.
Ex:- int, char, while, for, do, default, double
2. Operator: operators are symbols which tells operations is be performed on operands.
Tokens for the operators, either individually or in classes such as the token comparison
Ex:- + , * , / , >> , << , && , ||
3. Punctuation symbol: those are symbols generally used as word separator. Used Tokens
for each punctuation symbol, such as left and right parentheses, comma, and semicolon.
Ex:- ; , , :
4. Identifier: Name given to program element by the programmer.
Ex:- variable name, array name, function name, label
5. Constants: whose value does not change during program execution.
Constant
Numeric literal
When more than one lexeme can match a pattern, the lexical analyzer must provide the
subsequent compiler phase’s additional information about the particular lexeme that matched.
10
For example, the pattern for token number matches both 0 and 1, but it is extremely
important for the code generator to know which lexeme was found in the source program. Thus,
in many cases the lexical analyzer returns to the parser not only a token name, but an attribute
value that describes the lexeme represented by the token; the token name influences parsing
decisions, while the attribute value influences translation of tokens after the parse.
We shall assume that tokens have at most one associated attribute, although this attribute may
have a structure that combines several pieces of information. Usually compiler put that
information that attributes of token in terms of tables.
The most important example is the token id, where we need to associate with the token a
great deal of information. Normally, information about an identifier
1. its lexeme,
2. its type, and
3. The location at which it is first found (in case an error message about that identifier must
be issued) - is kept in the symbol table.
Thus, the appropriate attribute value for an identifier is a pointer to the symbol-table entry for
that identifier
For example compiler may create literal pool to store information about constants
[Link] name Type value line no
1 i integer numeric constant 5 3
2 f fractional numeric constant 3.14 5
3 ch character constant ‘c’ 7
4 str string constant “computer” 8
For identifier as variable name compiler creates symbol table
[Link] name type value Scope line No
1 j int 2 local (main) 10
For identifier as a function name compiler creates method table
[Link] name no of type of each return address of
parameters parameter type definition
1 add 2 int , int int 750
Note that in certain pairs, especially operators, punctuation, and keywords, there is no
need for an attribute value.
The token names and associated attribute values for the Fortran statement E = M * C ** 2
(** exponential operator) are written below as a sequence of pairs.
<id, pointer to symbol-table entry for E>
< assign-op >
<id, pointer to symbol-table entry for M>
<mult -op>
<id, pointer to symbol-table entry for C>
<exp-op>
<number , integer value 2 >
Input Buffering
Input buffering is used for speedup the task of reading source program. This task is made
difficult by the fact that we often have to look one or more characters beyond the next lexeme
before we can be sure we have the right lexeme
11
In C, single-character operators like -, =, or < could also be the beginning of a two-
character operator like ->, ==, or <=. Thus, we shall introduce a two-buffer scheme that handles
large lookaheads safely.
Buffer Pairs
Because of the amount of time taken to process characters and the large number of
characters that must be processed during the compilation of a large source program, specialized
buffering techniques have been developed to reduce the amount of overhead required to process
a single input character An important scheme involves two buffers that are alternately reloaded,
as suggested in Figure
E = M C * * 2 eof
lexemeBegin forward
Each buffer is of the same size N, and N is usually the size of a disk block, e.g., 1 kb.
Using one system command read we can read N characters into a buffer, rather than using one
system call per character.
If fewer than N characters remain in the input file, then a special character represented by
eof marks the end of the source file and is different from any possible character of the source
program.
Two pointers to the input are maintained:
1) Pointer lexemeBegin marks the beginning of the current lexeme, whose extent we are
attempting to determine.
2) Pointer forward scans ahead until a pattern match is found.
switch (*forward++ )
{ case eof:
if (forward is at end of first buffer )
{
reload second buffer;
forward = beginning of second buffer;
}
else if (forward is at end of second buffer )
{
reload first buffer;
forward = beginning of first buffer;
}
else /* eof within a buffer marks the end of input */
terminate lexical analysis;
break;
Cases for the other characters
}
Speciation of token
Regular expressions are important notation for specifying patterns. Each pattern matches
set of strings so , regular expression will serve as name for a set of strings.
1 LUD : Set of letters or digits.
2 LD : Set of strings consisting letters followed by digits.
3 L4 : Set of all four letter strings.
12
4 L* : Set of all strings of zero or more letters including.
5 L(L U D)* : Set of all strings of letters and digits beginning with letter.
6 D* : Set of all strings of zero or more digits including.
Recognition of token
Lexical analyzer will recognize the keyword if if
(if, else) then then
Lexemes denoted by relop (<,>,<=,>=) else else
Id (num I,j) relop < | >| <= | >= | <> | ==
id letter(letter | digit)*
num Const digit (.digit ) * (E (+|-) ? digit ) *
Regular expression Token attribute value
Ws - -
If If -
Else Else -
Id Id Pointer to symbol table entry
Num Num Pointer to symbol table entry
< relop LT
<= relop LE
<> relop NE
L,D
Start L *
q0 q1 q2
13
L = {A, B, C , . . ., Z, a, ,b, c, . . . , z, _ }
D = {0, 1, 2, 3, 4, 5, 6, 7, 8,9}
*= {space, : , , , ; , . . . .}
State q0 : ch = getchar ( ) ;
if (InputPtr== L ) then goto state q1 ;
else
error ( ) ;
State q1 : ch = getchar ( ) ;
if (InputPtr == L OR InputPtr == D) then goto state q1 ;
else if (InputPtr == *) then goto state q2 ; Design & Imp
else
error( );
State q2 : retract ( ) ;
return ( id, install ( ) ) ;
State q0 : ch = getchar ( ) ;
if (InputPtr== D) then goto state q1 ;
else
error ( ) ;
State q1 : ch = getchar ( ) ;
if (InputPtr == D) then goto state q1 ;
else if (InputPtr == .) then goto state q2 ;
else if (InputPtr == *) then goto state q4 ;
else
error( );
State q2 : ch = getchar ( ) ;
if (InputPtr == D) then goto state q3 ;
else
error();
State q3 : if (InputPtr == D) then goto state q3 ;
else if (InputPtr == *) then goto state q4 ;
else
error();
State q4 : retract ( ) ;
return ( id, install ( ) ) ;
14
[Link] ANALYSIS
Syntax analysis or parser is second phase of compiler. It is most important phase of compiler. This
extracts syntactical units the input stream.
Syntax analyzer works hand in hand with lexical analyzer. Lexical analyzer determines tokens
occurring in next input stream and returns same to the parser when asked for. Syntax analyzer considers
sequence of tokens for possible valid constructs of programming language.
Token
Source Lexical Parser Parse tre
program Analysis
GetNextToken Syntax error
Symbol
Table
Role of parser
1. To identify the language constructs presents in a given input program. If parser determines input
to be valid one, it outputs a representation of the input in the form of parse tree.
2. If input is grammatically incorrect, parser declares detection of syntax error in the input. In this
case parser cannot produce parse tree.
1. Lexical errors
Include misspellings of identifiers, keywords or operators or typographic mistakes.
Example the use of keyword els instead of else
2. Syntactic errors
These errors are occurred due to syntactical mistakes which may include misplaced semicolons or
extra or missing braces; As another example, in C or Java, the appearance of a case statement without an
enclosing switch is a syntactic error (however, this situation is usually allowed by the parser and caught
later in the processing, as the compiler attempts to generate code).
Example for(i=0 : i<10 : i++) instead of for(i=0 ; i<10 ; i++)
3. Semantic errors
Include type mismatches between operators and operands. It is also called static semantic erroers.
15
Example
1. float f = 3.1465;
int i = f; //assignment is not valid, int cannot store fractional part.
4. Logical errors
These errors are also called as exception or Runtime error or static semantic error. That occurs
during execution of the program.
Logical errors are not caught by compiler. It can be anything from incorrect reasoning on the part
of the programmer to the use in a C program of the assignment operator = instead of the comparison
operator ==. The program containing = may be well formed; however, it may not reflect the
programmer's intent.
Examples
1. array out of bound (accessing array index which beyond array size)
2. divide by zero (when divisor is zero)
3. stack overflow (incorrect recursive call to procedure)
Error-Recovery Strategies
Once an error is detected, how should the parser recover? Although no strategy has proven itself
universally acceptable, a few methods have broad applicability. The simplest approach is for the parser to
quit with an informative error message when it detects the first error. Additional errors are often
uncovered if the parser can restore itself to a state where processing of the input can continue with
reasonable hopes that the further processing will provide meaningful diagnostic information. If errors
pile up, it is better for the compiler to give up after exceeding some error limit than to produce an
annoying avalanche of "spurious" errors.
1. Panic-Mode Recovery
With this method, on discovering an error, the parser discards enough number of token to reach
to a descent state after detection of error. Parser moves in foreword direction until one of a designated
set of synchronizing tokens is not found. The synchronizing tokens are usually delimiters, such as
semicolon or closing brackets etc. The compiler designer must select the synchronizing tokens
appropriate for the source language. While panic-mode correction often skips a considerable amount of
input without checking it for additional errors, it has the advantage of simplicity.
2. Phrase-Level Recovery
On discovering an error, a parser may perform local correction on the remaining input; that is, it
may replace a prefix of the remaining input by some string that allows the parser to continue.
A typical local correction is to replace a comma by a semicolon, delete an extraneous semicolon, or
insert a missing semicolon.
The choice of the local correction is left to the compiler designer. Of course, we must be careful to
choose replacements that do not lead to generation of another error
3. Error Productions
By anticipating common errors that might be encountered, we can augment the grammar for the
language at hand with productions that generate the erroneous constructs. A parser constructed from a
grammar augmented by these error productions detects the anticipated errors when an error production
16
is used during parsing. The parser can then generate appropriate error diagnostics about the erroneous
construct that has been recognized in the input. This can be only done by compiler developer.
4. Global Correction
Ideally, we would like a compiler to make as few changes as possible in processing an incorrect
input string. There are algorithms for choosing a minimal sequence of changes to obtain a globally least-
cost correction.
Given an incorrect input string x and grammar G, these algorithms will find another string y
acceptable by G, such that the number of insertions, deletions, and changes of tokens required to
transform x into y is as small as possible. Unfortunately, these methods are in general too costly to
implement in terms of time and space, so these techniques are currently only of theoretical interest. Do
note that a closest correct program may not be what the programmer had in mind.
Left Recursion
A grammar is left recursive if it has a nonterminal A such that there is a A
derivation A => Aα for some string α.
A α
Top-down parsing methods cannot handle left-recursive grammars, so a
retransformation is needed to eliminate left recursion. A α
Left recursion may have two types .
Immediate left recursion .
General left recursion
Left Factoring
Left factoring is a grammar transformation that is useful for producing a grammar
Suitable for predictive parsing or top-down parsing. When the choice between two alternatives A
productions is not clear, we may be able to rewrite the productions to defer the decision until enough of
the input has been seen that we can make the right choice.
For example, if we have the two productions
17
A αβ| αγ (β = γ)
Then we can replace these productions by
A αA’
A’ β| γ
Top-Down Parsing
Top-down parsing can be viewed as the problem of constructing a parse tree for the input string,
starting from the root and creating the nodes of the parse tree in preorder (depth-first manner).
Equivalently, top-down parsing can be viewed as finding a leftmost derivation for an input string.
Consider the grammar
ScAd
Aab | b
To construct a parse tree top-down for the input string w = cad, begin with a tree consisting of a
single node labeled S, and the input pointer pointing to c, the first symbol of w. S has only one
production, so we use it to expand S and obtain the tree
S
c A d
The leftmost leaf, labeled c, matches the first symbol of input w, so we advance the input pointer
to a, the second symbol of w, and consider the next leaf, labeled A.
Now, we expand A using the first alternative A ab to obtain the tree
S
c A d
a b
We have a match for the second input symbol, a, so we advance the input pointer to d, the third
input symbol, and compare d against the next leaf, labeled b. Since b does not match d, we report failure
and go back to A to see whether there is another alternative for A that has not been tried, but that might
produce a match. In going back to A, we must reset the input pointer to position 2, the position it had
when we first came to A. this procedure is called as Backtracking.
For that the procedure for A must store the input pointer in a local variable. The second alternative
for A produces the tree
S
c A d
a
The leaf a matches the second symbol of w and the leaf d matches the third symbol. Since we have
produced a parse tree for w, we halt and announce successful completion of parsing.
Recursive-Descent Parsing
This parser consists of a set of recursive procedures with no backtracking.
For recursive descent parser, grammar should not contain
left recursion and
left factoring
if they are present first remove it. Then construct recursive procedures.
Steps foe constructing recursive procedures for productions AX1X2…..Xk
18
void A()
{
Choose an A-production, AX1X2…..Xk;
for ( i = l t o k )
{
if ( Xi is a nonterminal )
call procedure Xi () ;
else if ( Xi equals the current input symbol a )
advance the input to the next symbol;
else /* an error has occurred */;
}
}
A typical procedure for a nonterminal in a top-down parser a recursive-descent parsing program
consists of a set of procedures, one for each nonterminal. Execution begins with the procedure for the start
symbol, which halts and announces success if its procedure body scans the entire input string.
FALLOW(A)
Define FOLLOW(A), for nonterminal A, to be the set of terminals a that can appear immediately to
the right of A in some sentential form, that is, the set of terminals a such that there exists a derivation of
the form SαAaβ for some α and β. Note that there may, at some time during the derivation, have been
symbols between A and a, but if so, they derived ε and disappeared. If A can be the rightmost symbol in
some sentential form, then $, representing the input right endmarker, is in FOLLOW(A).
To compute FOLLOW(A) for all nonterminals A, apply the following rules
Until nothing can be added to any FOLLOW set:
1. Place $ in FOLLOW(S), where S is the start symbol and $ is the input right endmarker.
19
2. If there is a production A αBβ, then everything in FIRST(β), except for ε, is placed in FOLLOW(B).
It means FOLLOW(B) = FIRST(β) (FIRST(b) does not contain ε)
3. If there is a production A αB, or a production A aBβ where FIRST(β) contains ε (β ε) then
everything in FOLLOW(A) is in FOLLOW(B).
that is FOLLOW(B) = FOLLOW(A)
Predictive parser
1. Stack contains of sequence of grammar symbol. Initially stack contains $ followed by start
symbol where $ is bottom of stack marker.
1. Input string to parse fallowed by $, which is end of string marker.
2. Parsing table
Output is either accept if string is accepted by language or error.
To take actions it uses, top of the stack, symbol A and “a” is current input symbol. Parser take
following actions.
INPUT : grammar G
a b a a b $
Input
Predictive
X
parsing Output
program
Y
Predictive parsing Table
M
Z
Stack
20
Model of table driven predictive parser
METHOD: Initially, the parser is in a configuration with w$ in the input buffer and the start symbol S of
G on top of the stack, above $.
Below program uses the predictive parsing table M to produce a predictive parse for the input.
while ( X ≠ $ )
Example
SAB |€
AaAB|€
B bA
Consider string “aaabb”(valid) Consider string “abba”(invalid)
Stack Input Action Stack Input Action
$S Ababb$ SAB $S abba$ SAB
$BA Ababb$ AaAB $BA abba$ AaAB
$BBAa Ababb$ Pop a & ADVANCE $BBAa abba$ Pop a & ADVANCE
$BBA babb$ A€ $BBA bba$ A€
$BB babb$ BbA $BB bba$ BbA
$BAb babb$ Pop b & ADVANCE $BAb bba$ Pop b & ADVANCE
21
$BA abb$ A aAB $BA ba$ A €
$BBAa abb$ Pop a & ADVANCE $B ba$ BbA
$BBA bb$ A€ $Ab ba$ Pop b & ADVANCE
$BB bb$ B bA $A a$ AaAB
$BAb bb$ Pop b & ADVANCE $BAb a$ Pop a & ADVANCE
$BA b$ A€ $BA $ A€
$B b$ BbA $B $ ERROR
$Ab b$ Pop b & ADVANCE
$A $ A€
$ $ ACCEPT
LL(1) Grammars
Predictive parsers, that is, recursive-descent parsers needing no backtracking, can be constructed
for a class of grammars called LL(1).
The first "L" in LL(1) stands for scanning the input from left to right,
the second "L" for producing a leftmost derivation, and
the "1" for using one input symbol of lookahead at each step to make parsing action decisions.
The class of LL(1) grammars is rich enough to cover most programming constructs, although care
is needed in writing a suitable grammar for the source language.
For example, no left-recursive or ambiguous grammar can be LL(1).
A grammar G is LL(1) if and only if whenever A α | β are two distinct productions of G, the
following conditions hold:
1. For no terminal “a” do both α and β derive strings beginning with “a”.
2. At most one of α and β can derive the empty string.
3. If β ε then α does not derive any string beginning with a terminal in FOLLOW(A). Likewise, if α
ε then β does not derive any string beginning with a terminal in FOLLOW(A).
(The given grammar is LL(1) if in parsing table there is only one production per cell)
Bottom Up parser
Bottom Up parser build parser tree from bottom to top.
Parser starts with the string and finally reaches to start symbol.
22
Stack Input
$S w$
During a left-to-right scan of the input string, the parser shifts zero or more input symbols onto
the stack, until it is ready to reduce a string β of grammar symbols on top of the stack. It then reduces , β
to the head of the appropriate production. 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 $
Upon entering this configuration, the parser halts and announces successful completion of
parsing.
Handle
A handle is right sentential form γ is a production A β and the position of γ where the string β
may be found and replaced by A to production the previous rightmost derivation of γ.
Thus if S αAwαβw then Aβ in position following α is handle of αβw where w is string of
terminals.
If grammar is unambiguous then every right sentential form of grammar has exactly one
handle.
Handle Pruning
Right most derivation in reverse is obtained by handle pruning.
Bottom-up parsing during a left-to-right scan of the input constructs a rightmost derivation in
reverse. Informally, a "handle" is a substring that matches the body of a production, and whose reduction
represents one step along the reverse of a rightmost derivation.
The actions a shift-reduce parser might take in parsing the input string “aabbb” according to the
grammar
SaAb Right sentential form Handle Reducing production
AaA|Bb Aabbb B B b
Bb aaBbb Bb ABb
aaAb aA AaA
aAb aAb S aAb
Implementation of shift reduce parser
Shift reduce parser can be implemented using stack and input buffer.
Initially stack contain $(end marker of the stack) and input buffer contain string ending with $.
Parser takes two actions
1. Shifting zero or more symbols on the stack.
2. Reduce handle α to the left side of appropriate production.
Parser continues with any of the above action until input is empty and stack contains $. In this case parser
output successful compilation (accept) otherwise it outputs error.
Stack Input Action
$ aabbb$ Shift
$a abbb$ Shift
$aa Bbb$ Shift
$aab bb$ Reduce B b
$aaB bb$ Shift
$aaBb b$ Reduce A Bb
$aaA b$ Reduce A aA
$aA b$ Shift
$aAb $ Reduce SaAb
23
$S $ ACCEPT
LR Parsing: Simple LR
The most prevalent type of bottom-up parser today is based on a concept 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.
The cases k = 0 or k = 1 are of practical interest, and we shall only consider LR parsers with k ≤ 1
here. When (k) is omitted, k is assumed to be 1. This section introduces the basic concepts of LR parsing
and the easiest method for constructing shift-reduce parsers, called "simple LR" (or SLR, for short).
We begin with "items" and "parser states;" the diagnostic output from an LR parser generator
typically includes parser states, which can be used to isolate the sources of parsing conflicts.
There are different methods for constructing parsing table
Simple LR (SLR)
canonical-LR and
LALR –
That is used in the majority of LR parsers.
Why LR Parsers?
LR parsers are table-driven, much like the non-recursive LL parsers. Intuitively, for a grammar to
be LR it is sufficient that a left-to-right shift-reduce parser be able to recognize handles of right-sentential
forms when they appear on top of the stack.
Drawback of LR parser
The principal drawback of the LR method is that it is too much work to construct an LR parser by
hand for a typical programming-language grammar.
24
A specialized tool, an LR parser generator, is needed. Fortunately, many such generators are
available,
Note
One of the most commonly used tool is Yacc(Yet Another Compiler Complier), Such a
generator takes a context-free grammar and automatically produces a parser for that grammar. If the
grammar contains
Ambiguities or other constructs that are difficult to parse in a left-to-right scan of the input, then the
parser generator locates these constructs and provides detailed diagnostic messages.
Relation Meaning
25
a <· b a yields precedence to b
a =· b a has the same precedence as b
a ·> b a takes precedence over b
These operator precedence relations allow delimiting the handles in the right sentential forms:
<· marks the left end,
=· appears in the interior of the handle, and ·
·> marks the right end.
Let assume that between the symbols ai and ai+1 there is exactly one precedence relation. Suppose
that $ is the end of the string. Then for all terminals we can write: $ <· b and b ·> $.
For example, the following operator precedence relations can be introduced for simple
expressions:
Id + * $
id ·> ·> ·>
+ <· ·> <· ·>
* <· ·> ·> ·>
$ <· <· <· ·>
Example: The input string:
id1 + id2 * id3
after inserting precedence relations becomes
$ <· id1 ·> + <· id2 ·> * <· id3 ·> $
Having precedence relations allows to identify handles as follows:
scan the string from left until seeing ·>
scan backwards the string from right to left until seeing <·
everything between the two relations <· and ·> forms the handle
Note that not the entire sentential form is scanned to find the handle.
26
4. Syntax-Directed Definitions
Syntax directed definition is a generalization of a context free grammar in which each
grammar symbol has an associated set of attributes and semantic rules.
Attributes are associated with grammar symbols and rules are associated with
productions.
Evaluation of this semantic rule
Generate intermediate code
Generate error message
Put information into symbol table
Perform type checking
When we associate semantic rules with production, we use
Syntax Directed definition
Translation scheme
Syntax Directed definition
Give high level specifications for translations
Hide many implementation details such as evaluation of semantic actions
We associate a production rule with a set of semantic actions & we do not say when
they will be evaluated
Translation scheme
Indicate order of evaluation of semantic actions associated with production rule
Translation scheme gives little bit information about implementation details
Attributes may be of any kind:
numbers
data-types
table references
strings
for instance. The strings may even be long sequences of code, say code in the intermediate
language used by a compiler.
That attributes are partitioned into two subsets called as Synthesized attributes &
Inherited attributes
If X is a symbol and a is one of its attributes, then we write X.a to denote the value of a at a
particular parse-tree node labeled X. If we implement the nodes of the parse tree by records or
objects, then the attributes of X can be implemented by data fields in the records that represent
the nodes for X.
[Link] = 5 [Link] = 3
28
an annotated parse tree shows the values of attributes, a dependency graph helps us determine
how those values can be computed.
Dependency graph
A dependency graph depicts the flow of information among the attribute instances in a
particular parse tree; an edge from one attribute instance to another means that the value of the
first is needed to compute the second. Edges express constraints implied by the semantic rules.
In more detail:
For each parse-tree node, say a node labeled by grammar symbol X, the dependency
graph has a node for each attribute associated with X .
Suppose that a semantic rule associated with a production p defines the value of
synthesized attribute A.b in terms of the value of X.c (the rule may define A.b in terms of
other attributes in addition to X.c). Then, the dependency graph has an edge from X.c to
A.b. More precisely, at every node N labeled A where production p is applied, create an
edge to attribute b at N, from the attribute c at the child of N corresponding to this
instance of the symbol X in the body of the production
Suppose that a semantic rule associated with a production p defines the value of inherited
attribute B.c in terms of the value of X.a. Then, the dependency graph has an edge from
X.a to B.c. For each node N labeled B that corresponds to an occurrence of this B in the
body of production p, create an edge to attribute c at N from the attribute a at the node M
that corresponds to this occurrence of X. Note that M could be either the parent or a
sibling of N.
Example Consider the following production and rule:
At every node N labeled E, with children corresponding to the body of this production,
the synthesized attribute val at N is computed using the values of val at the two children, labeled
E and T. Thus, a portion of the dependency graph for every parse tree in which this production is
shown below.
As a convention, we shall show the parse tree edges as dotted lines, while the edges of the
dependency graph are solid.
E val
E val + T val
S-attributed definition
Syntax Directed Definition is S-attributed if every attribute is synthesized.
An attribute is synthesized if all its dependency point from child to parent in the parse
tree.
29
When a Syntax Directed Definition is S-attributed, we can evaluate its attributes in any
bottom up order of the nodes of the parse tree.
It is often especially simple to evaluate the attributes by performing a post-order traversal
of the parse tree and evaluating the attributes at a node N when the traversal leaves N for the last
time. That is, we apply the function post-order, defined below, to the root of the parse tree
postorder (N)
{ for ( each child C of N, from the left )
postorder(C);
evaluate the attributes associated with node N;
}
S-attributed definitions can be implemented during bottom-up parsing, since a bottom-up
parse corresponds to a post-order traversal. Specifically, post-order corresponds exactly to the
order in which an LR parser reduces a production body to its head. To evaluate synthesized
attributes and store them on the stack during LR parsing, without creating the tree nodes
explicitly.
Example desk top calculator.
L-attributed definition
L-attributed definition the attributes associated with a production body,
In dependency-graph edges can go from left to right, but not from right to left (hence "L-
attributed").
More precisely, each attribute must be either
1. Synthesized, or
2. Inherited, but with the rules limited as follows.
Suppose that there is a production AX1X2 … Xn, and that there is an inherited attribute Xi.a
computed by a rule associated with this production. Then the rule may use only:
1. Inherited attributes associated with the head A.
2. Either inherited or synthesized attributes associated with the occurrences of symbols X1,
X2,. . . , Xipl located to the left of Xi.
3. Inherited or synthesized attributes associated with this occurrence of Xi itself, but only in
such a way that there are no cycles in a dependency graph formed by the attributes of this
Xi
Example type declaration
30
This is called as Topological sort
If a dependency graph has edge from node X to node Y, then the attribute corresponding
to X must be evaluated before attribute of Y.
This method will fail to find an evaluation order only if the dependency graph has cycle.
Rule based method
Semantic rules associated with productions are analyzed by special tool during compiler
construction time.
Oblivious method
An evaluation order is chosen without considering the semantic rules and order of
evaluation is forced by parsing method
[Link]
[Link] [Link]
* [Link] + [Link]
+
* id
32
Any SDT can be implemented by first building a parse tree and then performing the
actions in a left-to-right depth-first order; that is, during a preorder traversal.
Typically, SDT's are implemented during parsing, without building a parse tree. Here we
focus on the use of SDT's to implement two important classes of SDD’s:
The underlying grammar is LR-parse able, and the SDD is S-attributed.
The underlying grammar is LL-parse able, and the SDD is L-attributed.
During parsing, an action in a production body is executed as soon as all the grammar symbols
to the left of the action have been matched. SDT's that can be implemented during parsing can be
characterized by introducing distinct marker non terminals in place of each embedded action;
each marker M has only one production, M €. If the grammar with marker non terminals can
be parsed by a given method, then the SDT can be implemented during parsing.
33
Each state entry is a pointer to LR(1) parsing table.
If the ith state symbol is X then val[i] will hold the value of the attribute associated
with the parse free node corresponding to X.
State Val
A A.a
top B B.b
C C.c
34
[Link]-Time Environments
A compiler must accurately translate the abstractions implemented in the source language
definition. These abstractions typically include the concepts such as names, scopes, bindings,
data types, operators, procedures, parameters, and flow-of-control constructs. The compiler must
cooperate with the operating system and other systems software to support these abstractions on
the target machine. To do so, the compiler creates and manages a run-time environment in which
it assumes its target programs are being executed.
This environment deals with a variety of issues such as the layout and allocation of
storage locations for the objects named in the source program, the mechanisms used by the target
program to access variables, the linkages between procedures, the mechanisms for passing
parameters, and the interfaces to the operating system, input/output devices, and other
programs.
Procedure
Procedure is block of statements associate with an identifier. The identifier is the name of
procedure, block of statement is procedure body.
Procedure that returns value is called as function.
When a procedure name appears within an executable statement, we can say that the
procedure is called at that point. The basic idea is that a procedure call executes the procedure
body.
A procedure is called as recursive if a new activation can begin before an earlier activation
of the same procedure has ended.
Activation of procedure
Each execution of the procedure body is referred as an activation of the procedure.
Lifetime of activation
The lifetime of procedure P is amount of computer time required to execute the
statements in the procedure.
Implementations of activation
We can implement activation by using two ways
1. Activation tree
2. Control stacks
1. Activation tree
1. Each node represents an activation of a procedure.
2. The root represents the activation of procedure main.
3. The node for procedure A is parent for node for procedure B if and only if control flows from
4. procedure A to procedure B.
5. The node for procedure A is to left to node for procedure B if and only if the lifetime
procedure A occurs before the lifetime of procedure B
2. Control stack
The of control in a program corresponds to depth-first search of the activation tree that start with
root, visits a node before it’s children & recursively visits children at each node left to right order.
We can use stack called control stack to keep track of live procedure activations. The idea
is to push the node for the activation onto control satck as activation begins & pop node when
activation ends.
35
int a [10] ; enter main ( )
void readArray() enter readArray ()
{ /* Reads 10 integers into a[0], ..., a[9]. */ leave readArray ()
int i ; enter quicksort ( 1 , 9)
for ( i = 0 ; i < 10 ; i ++ ) enter partition ( 1 , 9 )
{ read ( a[i]); } leave partition ( 1 , 9 )
} enter quicksort( l , 3 )
int partition(int my , int n) ...
{/* Picks a separator value u, and leave quicksort ( l , 3 )
partitions a[m .. n] so that a[m ..p - 11 are enter quicksort( 5 , 9 )
less than u, a[p] = u, and a[p + 1 .. n] are ...
equal to or greater than u. Returns p. */ leave quicksort( 5 , 9 )
} leave quicksort ( l , 9 )
void quicksort(int m, int n) leave main()
{ int i; main ( )
if (n > m)
{ i = partition(m, n);
readArray( ) q(1,9)
quicksort (my i-I) ;
quicksort (i+l, n) ;
} } p(1,9) q(1,3) q(5,9)
main()
{ readArray () ; p(1,3) q(1,0) q(2,3) q(5,9) p(5,5) q(7,9)
a[0] = -9999;
a[l0] = 9999;
p(2,3) q(2,1) q(3,3) p(7,9) q(7,7) q(9,9)
quicksort (1 , 9) ; }
Possible activations for the program of Quick sort
Activation record
The contents of activation records vary with the language being implemented. Here is a
list of the kinds of data that might appear in an activation record
[Link] The actual parameters used by the calling procedure. Commonly, these values
parameters are not placed in the activation record but rather in registers, when possible, for
greater efficiency. However, we show a space for them to be completely general.
[Link] Space for the return value of the called function, if any. Again, not all called
values procedures return a value, and if one does, we may prefer to place that value in
a register for efficiency
[Link] link pointing to the activation record of the caller
[Link] link may be needed to locate data needed by the called procedure but found
elsewhere, e.g., in another activation record
[Link] With information about the state of the machine just before the call to the
machine procedure. This information typically includes the return address (value of the
status program counter, to which the called procedure must return) and the contents
of general purpose registers that were used by the calling procedure and that
must be restored when the return occurs.
[Link] data Local variable belonging to procedure whose activation record this is.
[Link] Temporary values, those are generated by compiler for evaluating complex
expression.
Storage allocation strategies
Storage allocation means where we can keep variables
36
More generally there are three storage allocation strategies used in each programming
language, namely static allocation, heap allocation, stack allocation..
Static allocation lays out storage for all data objects at compile time.
Stack allocation manages the run-time storage as a stack.
Heap allocation allocates and de-allocates storages as needed at runtime from a data area
known as heap.
Static allocation
In static allocation, names are bound to storage as the program is complied, so there is no
need for a run-time support package. Since the bindings do not change at run time, every time a
procedure is activated, is names are bounded to the same storage locations. This property allows
the values of the local names to be retained across activations of a procedure. That is, when
control returns to a procedure, the values of the locals are the same as they were when control
left the last time.
From the datatype of a name (variable), the complier determines the amount of storage to
set aside for that name. The address of this storage consists of an offset from an end of the
activation record for the procedure. The complier must eventually decide where the activation
records, go relative to the target code. Ones this decision is made, the storage for each name in
the record is fixed. At compile time we can therefore fill in the addresses at which the target code
can find the data it operates on. Similarly the addresses at which information is to be saved when
a procedure call occurs are also known at compile time.
However, some limitations go along with using static allocations alone.
The size of the data object and constraints on its position in memory must be
known at compile time.
Recursive procedures are restricted, because all activations of a procedures use the
same bindings for local names.
Data structures cannot be created dynamically, since there is no mechanism for
storage allocation at run time.
Stack allocation
Stack allocation is based on the idea of control stack; storage is organized as a stack, and
activation records are pushed and popped as activations begin and end, respectively, storage for
the local variables in each call of a procedure is contained in the activation record from that call.
Thus locals are bound to fresh storage in each activation, because an new activation record is
pushed onto the stack when a call is made. Furthermore, the values of locals are deleted when
the activation ends; that is, the values are lost because the storage for locals disappears when the
activation record is popped.
Consider the case in which the sizes of all activation records are known at compile time.
Suppose that register top marks the top of the stack. At run time, an activation record can be
allocated and de-allocated by incrementing and decrementing top, respectively by the size of the
record. If procedure q has an activation record of size a, then top is incremented by a just before
the target code of q is executed. When control returns from q, top is decremented by a.
Heap allocation
We can make our program more flexible if, during execution, it could allocate additional
memory when needed and free memory when not needed. Allocation of memory during
37
execution is called dynamic memory allocation. C provides library functions to allocate and free
memory dynamically during program execution. Dynamic memory is allocated on the heap by
the system.
It is important to realize that dynamic memory allocation also has limits. If memory is
repeatedly allocated, eventually the system will run out of memory.
Two standard library functions are available for dynamic allocation. The function malloc()
allocates memory dynamically, and the function free() deallocates the memory previously
allocated by malloc(). When allocating memory, malloc() returns a pointer which is just a byte
address. As such, it does not point to an object of a specific type. A pointer type that does not
point to a specific data type is said to point to void type, i.e. the pointer is of type void *. In order
to use the memory to access a particular type of object, the void pointer must be cast to an
appropriate pointer type. Here are the descriptions for malloc(), and free():
malloc( ) free()
Prototype: void * malloc(unsigned size); void free (void * ptr);
Returns: void pointer to the allocated block of none
memory if successful, NULL otherwise
Description: Returned pointer must be cast to an ptr must be a pointer to previously
appropriate type allocated block of memory
38
Text segment,
Stack segment, and
Data segment.
Stack segment
: :
: : top
Fun1()
Data Dynamic Heap
segment data
region parameter
Fixed Non BSS top top
data main() main() main()
BSS
region
Code segment Text (a) (b) (c)
Symbol Table
Run rime sub division of process Organization of stack
(At run time stack grows in downward direction & heap grows in upward direction)
The text segment (sometimes also called the code segment) is where the compiled code of
the program itself resides. This is the machine language representation of the program steps to
be carried out, including all functions making up the program, both user defined and system.
The remaining two areas of system memory are where storage may be allocated by the
compiler for data storage. The stack is where memory is allocated for automatic variables within
functions. A stack is a Last In First Out (LIFO) storage device where new storage is allocated and
de allocated at only one ``end'', called the Top of the stack. This can be seen in above figure
When a program begins executing in the function main(), space is allocated on the stack
for all variables declared within main(), as seen in Figure (a). If main() calls a function, func1(),
additional storage is allocated for the variables in func1() at the top of the stack as shown in
Figure (b). Notice that the parameters passed by main() to func1() are also stored on the stack. If
func1() were to call any additional functions, storage would be allocated at the new Top of stack
as seen in the figure. When func1() returns, storage for its local variables is deallocated, and the
Top of the stack returns to to position shown in Figure (c). If main() were to call another function,
storage would be allocated for that function at the Top shown in the figure. As can be seen, the
memory allocated in the stack area is used and reused during program execution. It should be
clear that memory allocated in this area will contain garbage values left over from previous
usage.
The data segment provides more stable storage of data for a program; memory allocated
in the data remains in existence for the duration of a program.
There are further two parts in data segment
Fixed data segment (static data segment)
Dynamic data segment (Heap segment)
39
Therefore, global variables (storage class external), and static variables are allocated on the
Fixed data segment. Depending upon who is going to initializes the memory there are parts in
Fixed data segment
BSS (Block Started with Symbol):- The memory allocated in the Non BSS area, is
initialized to zero at program start by compiler, remains zero until the program makes use of it.
Thus, the heap area need not contain garbage.
Non BSS (Block not Started with Symbol):- initiation is done by programmer
Calling Sequences
Procedure calls are implemented by what are known as calling sequences, which consists of
code that allocates an activation record on the stack and
Enters information into its fields.
A return sequence is similar code to restore the state of the machine so the calling procedure can
continue its execution after the call. Calling sequences and the layout of activation records may
differ greatly, even among implementations of the same language.
The code in a calling sequence is often divided between the
calling procedure (the "caller") and
the procedure it calls (the "callee").
There is no exact division of run-time tasks between caller and callee; the source language,
the target machine, and the operating system impose requirements that may favor one solution
over another. In general, if a procedure is called from n different points, then the portion of the
calling sequence assigned to the caller is generated n times. However, the portion assigned to
the callee is generated only once. Hence, it is desirable to put as much of the calling sequence
into the callee as possible - whatever the callee can be relied upon to know. We shall see,
however, that the callee cannot know everything.
When designing calling sequences and the layout of activation records, the
Following principles are helpful:
1. Values communicated between caller and callee is generally placed at the beginning of the
callee’s activation record, so they are as close as possible to the caller's activation record. The
motivation is that the caller can compute the values of the actual parameters of the call and place
them on top of its own activation record, without having to create the entire activation record of
the callee, or even to know the layout of that record. Moreover, it allows for the use of
procedures that do not always take the same number or type of arguments, such as C's printf
function. The callee knows where to place the return value, relative to its own activation record,
while however many arguments are present will appear sequentially below that place on the
stack.
2. Fixed-length items are generally placed in the middle. Such items typically include the control
link, the access link, and the machine status fields. If exactly the same components of the
machine status are saved for each call, then the same code can do the saving and restoring or
each. Moreover, if we standardize the machine's status information, then programs such as
debuggers will have an easier time deciphering the stack contents if an error occurs.
40
3. Items whose size may not be known early enough are placed at the end of the activation
record. Most local variables have a fixed length, which can be determined by the compiler by
examining the type of the variable. However, some local variables have a size that cannot be
determined until the program executes; the most common example is a dynamically sized array,
where the value of one of the callee's parameters determines the length of the array. Moreover,
the amount of space needed for temporaries usually depends on how successful the code-
generation phase is in keeping temporaries in registers. Thus, while the space needed for
temporaries is eventually known to the compiler, it may not be known when the intermediate
code is first generated.
4. We must locate the top-of-stack pointer judiciously. A common approach is to have it point to
the end of the fixed-length fields in the activation record. Fixed-length data can then be accessed
by fixed offsets, known to the intermediate-code generator, relative to the top-of-stack pointer. A
consequence of this approach is that variable-length fields in the activation records are actually
"above" the top-of-stack. Their offsets need to be calculated at run time, but they too can be
accessed from the top-of- stack pointer, by using a positive offset.
The calling sequence and its division between caller and callee is as follows:
1. The caller evaluates the actual parameters.
2. The caller stores a return address and the old value of top into the callee's activation record.
The caller then increments top & top is moved past the caller's local data and temporaries and
the callee's parameters and status fields.
3. The callee saves the register values and other status information.
4. The callee initializes its local data and begins execution.
A suitable, corresponding return sequence is:
1. The callee places the return value next to the parameters
2. Using information in the machine-status field, the callee restores top and other registers, and
then branches to the return address that the caller placed in the status field.
3. Although top has been decremented, the caller knows where the return value is, relative to the
current value of top-sp; the caller therefore may use that value.
The above calling and return sequences allow the number of arguments of the called
procedure to vary from call to call (e.g., as in C's printf function).
Note that at compile time, the target code of the caller knows the number and types of
arguments it is supplying to the callee. Hence the caller knows the size of the parameter area.
The target code of the callee, however, must be prepared to handle other calls as well, so it waits
until it is called and then examines the parameter field. Using the organization information
describing the parameters must be placed next to the status field, so the callee can find it. For
example, in the printf function of C, the first argument describes the remaining arguments, so
once the first argument has been located, the caller can find whatever other arguments there are.
Garbage Collectors
Garbage collection is the reclamation of chunks of storage holding objects that can no
longer be accessed by a program. We need to assume that objects have a type that can be
determined by the garbage collector at run time. From the type information, we can tell how
large the object is and which components of the object contain references (pointers) to other
objects. We also assume that references to objects are always to the address of the beginning of
the object, never pointers to places within the object. Thus, all references to an object have the
41
same value and can be identified easily. A user program, which we shall refer to as the mutator,
modifies the collection of objects in the heap. The mutator creates objects by acquiring space
from the memory manager, and the mutator may introduce and drop references to existing
objects. Objects become garbage when the mutator program cannot "reach” them. The garbage
collector finds these unreachable objects and reclaims their space by handing them to the
memory manager, which keeps track of the free space. A Basic Requirement: Type Safety
Not all languages are good candidates for automatic garbage collection. For a garbage
collector to work, it must be able to tell whether any given data element or component of a data
element is, or could be used as, a pointer to a chunk of allocated memory space. A language in
which the type of any data component can be determined is said to be type safe. There are type-
safe languages like ML, for which we can determine types at compile time. There are other type-
safe languages, like Java, whose types cannot be determined at compile time, but can be
determined at run time. The latter are called dynamically typed languages. If a language is
neither statically nor dynamically type safe, then it is said to be unsafe. Unsafe languages, which
unfortunately include some of the most important languages such as C and C++, are bad
candidates for automatic garbage collection. In unsafe languages, memory addresses can be
manipulated arbitrarily: arbitrary arithmetic operations can be applied to pointers to create new
pointers, and arbitrary integers can be cast as pointers. Thus a program theoretically could refer
to any location in memory at any time. Consequently, no memory location can be considered to
be inaccessible, and no storage can ever: be reclaimed safely.
In practice, most C and C++ programs do not generate pointers arbitrarily, and a theoretically
unsound garbage collector that works well empirically has been developed and used. For finding
garbage in unsafe languages like C, C++ there are verity of algorithms are developed one of
them is “Mark & Swap” algorithm
Void demo ()
{ int *p;
p = (int *) malloc(4);
In above code p is local pointer that will get memory on stack but by using p we created memory
on heap and we know memory on heap remains trough out life time process. After function call
is done p will get reclaimed successfully but memory pointed by p is not get reclaimed. This is
nothing but garbage.
Dangling references
Whenever storage can be de-allocated, the problem of dangling references arises.
A dangling reference occurs when there is reference to storage that has been de-allocated.
It is logical error to use dangling references, since the value of de-allocated storage is undefined
according to semantic rules of most of the languages.
It may be possible that your pointer referred memory may be given to another one, and then
access that memory become memory violation.
int *p,*q; Now p & q both are pointing to same memory location. After doing call to
p = (int *) free( p ), memory referred by p is get de-allocated. Then q is referring to
42
malloc(4); such memory location which is not valid. Such pointer q is called dangling
q = p; reference
free(p);
43
about a name to be kept outside the table entry, with only pointer to this information stored in
the record.
Each scheme is evaluated on the basis of time required to add n entries and make e
inquires. A linear lists the simplest to implement, but its performance is poor when n and e are
large, Hashing schemes provide better performance for greater programming effort and space
overhead.
It is useful for a compiler to be able to grow the symbol table dynamically at compile time.
If the symbol table is fixed when the compiler is written, the size must be chosen large enough to
handle any source program that might be presented.
For each mechanism we require following functions
44
double d; 2 ch char Local to display 20
char ch; 3 f folat Global 5
4 d double Global 6
1 (double)
d
1 (char)
x c
45
[Link] CODE GENERATION
In the process of translating a source program into target code, a compiler may construct one or
more intermediate representations, which can have a variety of forms like
Postfix notation
Syntax tree
Three address code
Syntax trees are one of the forms of intermediate representation; they are commonly used during
syntax and semantic analysis. After syntax and semantic analysis of the source program, many compilers
generate an explicit low-level or machine-like intermediate representation, which we can think of as a
program for an abstract machine.
Graphical representation
A syntax tree depicts the natural hierarchical structure of a source program.
Syntax tree can be implemented using two ways
Array representation
Link list representation
For list representation there are functions called mknode( ) & mkleaf ( ) which creates new node and
returns pointer of newly created node. Mknode ( ) & mkleaf ( ) function has two different forms
Mknode(operator , argument1 , argument2 ) for create node for binary operator like + , -, * , / .
Mknode(operator , argument1 ) for create node for unary operators like unary + , unary - ,
increment operator ++ , decrement operator - - ,bit-wise operator ~.
Mkleaf(id , attribute) to create node for identifier like variables.
(Attribute is pointer symbol table entry for respective variable).
Mkleaf (const, attribute) to create node for constant.
(Attribute is pointer literal pool entry for respective constant).
To represent syntax tree in the form of list representation we should first generate semantic
productions to list out pointers and associated nodes
46
[Link] Operation Arg1 Arg2 =
0 Id b
1 Id c
2 - 1 a +
3 * 0 2
4 Id b
5 Id c * *
6 - 4
7 * 4 6
8 + 3 7 b - b -
9 Id A
10 = 9 8
c c
id(a) \0 +
* *
id(b) \0 - id(b) -
id(c) \0 id( c) \0
A DAG (Direct Acyclic Graph) gives the same information but in more compact way because sub-
expressions are identified.
Semantic productions for DAG (Direct Acyclic Graph)
47
Production Semantic rules
S E1 = E2 [Link] = mknode (“=” , [Link] , [Link])
E1 id ( a ) [Link] = mkleaf ( id , pointer to symbol table entry for a )
E2 E3 + E3 [Link] = mknode (“+” , [Link] , [Link])
E3 E4 * E5 [Link] = mknode (“*” , [Link] , [Link])
E4 id ( b ) [Link] = mkleaf ( id , pointer to symbol table entry for b )
E5 - E6 [Link] = mknode (“-” , [Link] )
E6 id ( c ) [Link] = mkleaf ( id , pointer to symbol table entry for c )
=
=
a +
id(a) \0 +
*
*
b -
id(b) -
. c
id( c) \0
2. Assignments of the form x = op y, where op is a unary operation. Essential unary operations include
unary minus, logical negation, shift operators, and conversion operators that, for example, convert an
integer to a floating-point number.
4. An unconditional jump goto L. The three-address instruction with label L is the next to be executed.
5. Conditional jumps of the form if x goto L. These instructions execute the instruction with label L next
if x is true. Otherwise, the following three-address instruction in sequence is executed next, as usual.
6. Procedure calls and returns are implemented using the following instructions: param x for parameters;
c a l l p , n and y = c a l l p , n for procedure and function calls, respectively; and return y, where y,
representing a returned value, is optional. Their typical use is as the sequence of three address
instructions
param x, call p, n
generated as part of a call of the procedure p(xl,x2,. . . ,x,). The integer n, indicating the number of actual
parameters in "call p, n," is not redundant because calls can be nested. That is, some of the first param
statements could be parameters of a call that comes after p returns its value; that value becomes another
parameter of the later call. The implementation of procedure calls
7. Indexed copy instructions of the form x = y[i] and x[i] = y. The instruction x = y[i] sets x to the value in
the location i memory units beyond location y . The instruction x[i] = y sets the contents of the location i
units beyond x to the value of y.
8. Address and pointer assignments of the form *x = & y sets the r-value of x to be the location (l-value)
of y,
48
Each three-address assignment instruction has at most one operator on the right side. Thus, these
instructions fix the order in which operations are to be done.
The compiler must generate a temporary name to hold the value computed by a three-address
instruction.
Some "three-address instructions" can have fewer operands than three operands.
Three address code instructions contain at the most three addresses. These instructions can be
implemented as objects or as records with fields for the operator and the operands.
There are three such representations are called
Quadruples,
Triples, and
Indirect Triples.
Quadruples
A quadruple (or just "quad') has four fields, which we call operator, argument1, argument2, and
result. The operator field contains an internal code for the operator.
For instance, the three-address instruction x = y + z is represented by placing + in operator, y in
argument1, z in argument2, and x in result. The following are some exceptions to this rule
I. Instructions with unary operators like x = - y or x = y do not use argument2. Note that for a copy
statement like x = y, op is =, while for most other operations, the assignment operator is implied.
2. Operators like param use neither arg2 nor result.
3. Conditional and unconditional jumps put the target label in result.
Triples
A triple has only three fields, which we call operator, argument1, and argument2. Note that the
result field in quadruples is used primarily for temporary names. Using triples, we refer to the result of an
operation x op y by its position, rather than by an explicit temporary name. Thus, instead of the
temporary tl in quadruples, a triple representation would refer to position (0). Parenthesized numbers
represent pointers into the triple structure itself.
Indirect Triples
Indirect triples consist of a listing of pointers to triples, rather than a listing of triples themselves.
With indirect triples, an optimizing compiler can move an instruction by reordering the instruction list,
without affecting the triples themselves.
Consider expression c = ( a + b ) * ( a + b ) ;
Quadruples Triples
[Link] operator Arg 1 Arg 2 Result [Link] operator Arg 1 Arg 2
1 + a B t1 1 + a b
2 + a B t2 2 + a b
3 * t1 t2 c 3 * 1 2
Indirect Triples
Pointer list Triples
[Link] Pointer Statement [Link] Operator Arg 1 Arg 2
1 (0) 10 10 + a b
2 (1) 11 11 + a b
3 (2) 12 12 * 1 2
Declarations
We shall study types and declarations using a simplified grammar that declares just one name at a
time; the context free grammar (CFG) for declarations is
D T id ; D | € Nonterminal D generates a sequence of declarations. Nonterminal T generates
T B C | struct { D } basic, array, or record types. Nonterminal B generates one of the basic type’s int
B int | float and float. Nonterminal C, for "component," generates strings of zero or more
49
C €| [num] C integers, each integer surrounded by brackets. An array type consists of a basic
type specified by B, followed by array components specified by non-terminal C.
A record type (the second production for T) is a sequence of declarations for the fields of the
record, all surrounded by curly braces.
Boolean expression
In programming languages Boolean expression has two primary purposes
They are used to compute logical values
They are used as conditional expression
that are appear after the flow of control such that, if, if … else, while statement.
Boolean expressions are composed as Boolean operator AND(&&), OR(||), NOT(~) applied to
Boolean variables or relational expression.
Consider Boolean expression generating the following grammar
EEORE | E AND E | NOT E | (E) | E relop E | id | true | false
The array type int [2] [3] can be read as "array of 2 arrays of 3 integers each" and written as a type
expression array(2, array(3, integer)).
This type is represented by the tree in below figure. The operator [ ] takes two parameters, a number and
a type.
Procedure calls
Procedure or function is an important programming construct which is used to obtain the
modularity in the user program
Consider the grammar for a simple procedure call
S call id(L) // S denote statement to call procedure (id is procedure name)
L L, E // L denotes parameter list
E id1 // E denotes parameter name
Back patching
51
A key problem when generating code for Boolean expressions and flow-of-control statements is
that of matching a jump instruction with the target of the jump.
For example, the translation of the Boolean expression B in if ( B ) then S ; contains a jump, for
when B is false, to the instruction following the code for S. In a one-pass translation, B must be translated
before S is examined. What then is the target of the goto that jumps over the code for S ? For that purpose
a separate pass is then needed to bind labels to addresses. This section takes a complementary approach,
called backpatching, in which lists of jumps are passed as synthesized attributes. Specifically, when a
jump is generated, the target of the jump is temporarily left unspecified. Each such jump is put on a list of
jumps whose labels are to be filled in when the proper label can be determined. All of the jumps on a list
have the same target label.
If we see the three address code, labels L1 & L2 are get used first then they are get defined, this is
called as foreword declaration. One pass complier can not generate code for foreword declared
instruction. For that purpose we require two pass compiler. two pass compiler prepare one table that
contain labels and fill addresses during first pass, and in second pass it replace the labels by its
corresponding address in the table.
52
[Link] Generation
The final phase of compiler model is the code generator. It takes as input the intermediate
representation (IR) produced by the front end of the compiler, along with relevant symbol table
information, and produces as output a semantically equivalent target program
The requirements imposed on a code generator are severe.
The target program must preserve the semantic meaning of the source program and be of
high quality; that is, it must make effective use of the available resources of the target
machine.
The code generator itself must run efficiently.
The challenge is that, generating an optimal target program for a given source program is un
decidable; many of the sub-problems encountered in code generation such as register allocation
are computationally intractable. In practice, we must be content with heuristic techniques that
generate good, but not necessarily optimal, code.
Compilers that need to produce efficient target programs, include an optimization phase
prior to code generation. The optimizer maps the IR into IR from which more efficient code can
be generated.
In general, the code optimization and code-generation phases of a compiler, often referred to
as the back end, may make multiple passes over the IR before generating the target program.
53
We also assume that all syntactic and static semantic errors have been detected, that the
necessary type checking has taken place, and that type conversion operators have been inserted
wherever necessary.
The code generator can therefore proceed on the assumption that its input is free of these
kinds of errors.
Target program
Output of code generator is target program. Target program may take verity of forms
Absolute machine language
Re-locatable machine language
Assembly language
Absolute machine language
Producing absolute machine language program as output has more advantage that it can
be places directly in memory and immediately executed.
Languages WATFIV , PL/C produce Absolute machine language as output.
Re-locatable machine language
Producing re-locatable machine language program as output allows sub-program to be
complied separately. A set of re-locatable object modules can be linked together and loaded in
memory for execution. Here we pay some extra time for linking and loading. But if we produce
re-locatable object module we can store it as library and cal it in different programs.
If target machine does not handle relocation then compiler should explicitly provide
relocation information to loader to link the separately compiled program segments.
Assembly language
Producing Assembly language as output is much easier. We can generate symbolic
instructions and use macro facilities of assembler to generate target code. The assembly step
is done after code generation.
Instruction Selection
The code generator must map the IR program into a code sequence that can be executed
by the target machine. The complexity of performing this mapping is determined by a factor
such as 1. The level of the IR
2. The nature of the instruction-set architecture
3. The desired quality of the generated code.
If the IR is high level, the code generator may translate each IR statement into a sequence
of machine instructions using code templates. Such statement by statement code generation,
however, often produces poor code that needs further optimization.
If the IR reflects some of the low-level details of the underlying machine, then the code
generator can use this information to generate more efficient code sequences.
The nature of the instruction set of the target machine has a strong effect on the difficulty
of instruction selection.
For example, the uniformity and completeness of the instruction set are important factors.
If the target machine does not support each data type in a uniform manner, then each
exception to the general rule requires special [Link] some machines, floating-point
operations are done using separate registers. Instruction speeds and machine idioms are other
important factors. If we do not care about the efficiency of the target program, instruction
54
selection is straightforward. For each type of three-address statement, we can design a code
skeleton that defines the target code to be generated for that construct.
For example, if the target machine has an "increment" instruction (INC), then the three-
address statement a = a + 1 may be implemented more efficiently by the single instruction INC a,
rather than by a more obvious sequence that loads a into a register, adds one to the register, and
then stores the result back into a:
Evaluation Order
55
The order in which computations are performed can affect the efficiency of the target
code. As we shall see, some computation orders require fewer registers to hold intermediate
results than others. However, picking a best order in the general case is a difficult NP-complete
problem. Initially, we shall avoid the problem by generating code for the three-address
statements in the order in which they have been produced by the intermediate code generator.
Target language
The target machine and its instruction set is a prerequisite for designing a good code
generator. We shall use as a target language assembly code for a simple computer that is
representative of many register machines.
The instruction-set architecture of the target machine has a significant impact on the
difficulty of constructing a good code generator that produces high-quality machine code.
The most common target-machine architectures are
A RISC machine typically has many registers, three-address instructions, simple addressing
modes, and a relatively simple instruction-set architecture.
In contrast, a CISC machine typically has few registers, two-address instructions, a variety of
addressing modes, several register classes, variable-length instructions, and instructions with
side effects.
In a stack-based machine, operations are done by pushing operands onto a stack and then
performing the operations on the operands at the top of the stack. To achieve high
performance the top of the stack is typically kept in registers. Stack-based machines almost
disappeared because it was felt that the stack organization was too limiting and required too
many swap and copy operations.
We shall use as a target language assembly code for a simple computer that is
representative of many register machines. Our target computer models a three-address machine
with load and store operations, computation operations, jump operations, and conditional
jumps. The underlying computer is a byte-addressable machine with n general-purpose
registers, RO, R1, . . . , Rn - 1. We consider in our assembly language we shall use a very limited
set of instructions and assume that all operands are integers. Most instructions consists of an
operator, followed by a list of source operands. A label may precede an instruction.
56
The most common form of this instruction is LOD r, x which loads the value in location x
into register r.
An instruction of the form LOD rl, r2 is a register-to-register copy in which the contents of
register r 2 are copied into register rl.
Store operations: (STO x, r )stores the value in register r into the location x.
This instruction denotes the assignment x = r.
Computation operations of the form OP X , Y
where OP is a operator like ADD or SUB, and X, Y are locations, not necessarily distinct. The
effect of this machine instruction is to apply the operation represented by OP to the values in
locations X and Y, and place the result of this operation in register AX.
For example, SUB a, b computes AX = a - b
Unary operators that take only one operand do not have a Y
Unconditional jumps: (JMP L) causes control get transferred to the machine instruction
with label L.
Conditional jumps of the form if(cond for r) then jmp L, where r is a register, L is a label,
and cond stands for any of the common tests on values in the register r.
For example, if( r < 0) then jmp L causes a jump to label L if the value in register r is
less than zero, and allows control to pass to the next machine instruction if not.
57
2. The instruction LOD AX, M loads the contents of memory location M into register AX.
The cost is two since the address of memory location M is in the word following the
instruction.
3. The instruction LOD AX, *100(BX) loads into register AX the value given by
contents(contents(l00 + contents(BX))). The cost is three because the constant 100 is stored
in the word following the instruction.
Basic Blocks
Our first job is to partition a sequence of three-address instructions into basic blocks. We
begin a new basic block with the first instruction and keep adding instructions until we meet
either a jump, or a conditional jump, or a label on the following instruction. In the absence of
jumps and labels, control proceeds sequentially from one instruction to the next. This idea is
formalized in the following algorithm.
Algorithm: Partitioning three-address instructions into basic blocks.
INPUT: A sequence of three-address instructions.
OUTPUT: A list of the basic blocks for that sequence in which each instruction is assigned to
exactly one basic block.
METHOD: First, we determine those instructions in the intermediate code that are leaders, that
is, the first instructions in some basic block. The instruction just past the end of the intermediate
program is not included as a leader. The rules for finding leaders are:
1. The first three-address instruction in the intermediate code is a leader.
2. Any instruction that is the target of a conditional or unconditional jump is a leader.
3. Any instruction that immediately follows a conditional or unconditional jump is a leader.
Then, for each leader, its basic block consists of itself and all instructions up to but not including
the next leader or the end of the intermediate program.
Source code to set a 10 x 10 matrix to an identity matrix
for i from 1 to 10 do
{
for j from 1 to 10 do
{
58
a[i, j] = 0.0;
}
}
for i from 1 to 10 do
{
a[i, i] = 1.0;
}
The intermediate code turns a 10 x 10 matrix ‘a’ into an identity matrix. In generating the
intermediate code, we have assumed that the real-valued array elements take 2 bytes each, and
that the matrix ‘a’ is stored in row-major form.
1 i=1
2 j=1
3 t1 = 10 * i
4 t2 = t1 + j
5 t3 = 2 * t2
6 t4 = t3 – 22
7 a[t4] = 0
8 j=j+1
9 if j <= 10 goto (3)
10 i =i+1
11 if i <= 10 goto (2)
12 i=1
13 t5 = i – 1
14 t6 = 88 * t5
15 a[t6] = 1
16 i=i+1
17 if i <= 10 goto (13)
Source code to set a 10 x 10 matrix to an identity matrix
59
For leader 13 the block is 13 through 17.
Flow Graphs
Once an intermediate-code program is partitioned into basic blocks, we represent the flow
of control between them by a flow graph. The nodes of the flow graph are the basic blocks. There
is an edge from block B to block C if and only if it is possible for the first instruction in block C to
immediately follow the last instruction in block B. Thete are two ways that such an edge could be
justified:
There is a conditional or unconditional jump from the end of B to the beginning of C.
C immediately follows B in the original order of the three-address instructions, and B does
not end in an unconditional jump.
We say that B is a predecessor of C, and C is a successor of B.
Often we add two nodes, called the entry and exit that do not correspond to executable
intermediate instructions. There is an edge from the entry to the first executable node of the flow
graph, that is, to the basic block that comes from the first instruction of the intermediate code.
There is an edge to the exit from any basic block that contains an instruction that could be the last
executed instruction of the program. If the final instruction of the program is not an
unconditional jump, then the block containing the final instruction of the program is one
predecessor of the exit, but so is any basic block that has a jump to code that is not part of the
program.
In the flow graph, it is normal to replace the jumps to instruction numbers or labels by
jumps to basic blocks. Recall that every conditional or unconditional jump is to the leader of
some basic block, and it is to this block that the jump will now refer. The reason for this change is
that after constructing the flow graph, it is common to make substantial changes to the
instructions in the various basic blocks. If jumps were to instructions, we would have to fix the
targets of the jumps every time one of the target instructions was changed.
Flow graphs, being quite ordinary graphs, can be represented by any of the data
structures appropriate for graphs. The content of nodes (basic blocks) need their own
representation. We might represent the content of a node by a pointer to the leader in the array
of three-address instructions, together with a count of the number of instructions or a second
pointer to the last instruction. However, since we may be changing the number of instructions in
a basic block frequently, it is likely to be more efficient to create a linked list of instructions
for each basic block.
Example: The set of basic blocks constructed in Example 8.6 yields the flow graph of Fig. 8.9. The
entry points to basic block B1, since B1 contains the first instruction of the program. The only
successor of B1 is B2, because B1 does not end in an unconditional jump, and the leader of B2
immediately follows the end of B1.
Block B3 has two successors. One is itself, because the leader of B3, instruction3, is the target of
the conditional jump at the end of B3, instruction 9. The other successor is B4, because control
can fall through the conditional jump at the end of B3 and next enter the leader of B4.
Only Bs points to the exit of the flow graph, since the only way to get to code that follows the
program from which we constructed the flow graph is to fall through the conditional jump that
ends B6.
60
ENTRY
B1 i=1
B2 j=1
t1 = 10 * i
t2 = t1 + j
t3 = 2 * t2
t4 = t3 – 22
B3
a[t4] = 0
j=j+1
if j <= 10 goto B3
i =i+1
B4
if i <= 10 goto B2
B5 i=1
t5 = i – 1
t6 = 88 * t5
B6 a[t6] = 1
i=i+1
if i <= 10 goto B6
EXIT
Next-Use Information
Knowing when the value of a variable will be used next is essential for generating good
code. If the value of a variable that is currently in a register will never be referenced
subsequently, then that register can be assigned to another variable.
use of a name in a three-address statement is defined as follows. Suppose three-address
statement i assigns a value to x. If statement j has x as an operand, and control can flow from
statement i to j along a path that has no intervening assignments to x, then we say statement j
uses the value of x computed at statement i. We further say that x is live at statement i.
61
We wish to determine for each three-address statement x = y + z what the next uses of x,
y, and z are. For the present, we do not concern ourselves with uses outside the basic block
containing this three-address statement.
Our algorithm to determine live ness and next-use information makes a backward pass
over each basic block. We store the information in the symbol table. We can easily scan a stream
of three-address statements to find the ends of basic blocks as in Algorithm 8.5. Since procedures
can have arbitrary side effects, we assume for convenience that each procedure call starts a new
basic block.
Algorithm: Determining the liv-ness and next-use information for each statement in a basic
block.
INPUT: A basic block B of three-address statements. We assume that the symbol table initially
shows all non-temporary variables in B as being live on exit.
OUTPUT: At each statement i: x = y + z in B, we attach to i the live ness and next-use
information of x, y, and z.
METHOD: We start at the last statement in B and scan backwards to the beginning of B. At each
statement i: x = y + z in B, we do the following:
1. Attach to statement i the information currently found in the symbol table regarding the
next use and live ness of x, y, and y.
2. In the symbol table, set x to "not live" and "no next use."
3. In the symbol table, set y and z to "live" and the next uses of y and z to i.
Here we have used + as a symbol representing any operator. If the three-address statement i is of
the form x = + y or x = y, the steps are the same as above, ignoring z. Note that the order of steps
(2) and (3) may not be interchanged because x may be y or x.
The DAG representation of a basic block lets us perform several code improving
transformations on the code represented by the block.
1. We can eliminate local common sub-expressions, that is, instructions that compute a value
that has already been computed.
2. We can eliminate dead code, that is, instructions that compute a valuethat is never used.
3. We can reorder statements that do not depend on one another; such reordering may
reduce the time a temporary value needs to be preserved in a register.
4. We can apply algebraic laws to reorder operands of three-address instructions, and
sometimes t hereby simplify t he computation.
Basic block DAG
62
+ c
a=b+c
b=a–d - b, d
c=b+c
d=a–d + a d
b c
We assume that the basic block has already been transformed into a preferred sequence of
three-address instructions, by transformations such as combining common sub-expressions.
We further assume that for each operator, there is exactly one machine instruction that
takes the necessary operands in registers and performs that operation, leaving the result in a
register. The machine instructions are of the form
LD reg, mem
ST mem, reg
OP reg, reg, reg
Design of the Function getReg( )
Let us consider how to implement getReg(I), for a three-address instruction
Consider x = y + x as the generic example. First, we must pick a register for y and a register for x.
The issues are the same, so we shall concentrate on picking register Ry for y. The rules are as
follows:
63
1. If y is currently in a register, pick a register already containing y as R. Do not issue a machine
instruction to load this register, as none is needed.
2. If y is not in a register, but there is a register that is currently empty, pick one such register as
Ry.
3. The difficult case occurs when y is not in a register, and there is no register that is currently
empty. We need to pick one of the allowable registers anyway, and we need to make it safe to
reuse. Let R is that register, and suppose v is one of the variables in that the register. We need to
make sure that value of v either is not really needed, or that there is somewhere else we can go to
get the value of R.
64
[Link] OPTIMIZATION
Elimination of unnecessary instructions in object code, or the replacement of one sequence of
instructions by a faster sequence of instructions that does the same thing is usually called "code
improvement" or "code optimization."
Copy propagation
65
Copy propagation is the process of replacing the occurrences of targets of direct assignments with their
values. A direct assignment is an instruction of the form x = y, which simply assigns the value of y to x.
Copy propagation operates on a low-level intermediate representation such as quads or registers transfer
level, and can operate on either the basic block or control flow graph level.
Example 1 Example 2
Before optimization After optimization
Before optimization After optimization
If (a < b) If (a < b)
y = x; z=3+x
{ d=a+b; { t=a+b;
z = 3 + y;
} d=t;
Else }
{ e=a+b; Else
} { t=a+b;
c=a+b; e=t;
}
c=t;
Dead-Code Elimination
A variable is live at a point in a program if its value can be used subsequently;
Otherwise, it is dead at that point. A related idea is dead (or useless) code-statements that compute values
that never get used. While the programmer is unlikely to introduce any dead code intentionally, it may
appear as the result of previous transformations.
For example
#define SIZE 1
In any programming language 0 is considered as false and any non zero value
If(SIZE)
{ -------- considered as true. In this code else is never get executed because value of SIZE
}
is 1, 1 considered as true it means every time if is get executed because
Else
{ ------------------- expression inside if results true
}
Loop optimization
Loops are a very important place for optimizations, especially the inner loops where programs
tend to spend the bulk of their time. The running time of a program may be improved if we decrease the
number of instructions in an inner loop, even if we increase the amount of code outside that loop.
Three techniques are important for loop optimization
Code motion (which moves code outside a loop)
Induction variable elimination
Strength reduction (replace higher operator by cheaper one)
Code motion
An important modification that decreases the amount of code in a loop is code motion. This
transformation takes an expression that yields the same result independent of the number of times a loop
is executed (a loop-invariant computation) and evaluates the expression before the loop. Note that the
notion "before the loop" assumes the existence of an entry for the loop,
Before Time After optimization Time Evaluation of c=5 is a
optimization complexity complexity loop-invariant computation in
While (I < 10) 10 C=5; 1 the above while statement
{ C=5; 10 While (I < 10) 10 Code motion will result in the
Printf(“%d”,i); 10 { Printf(“%d”,i); 10
equivalent code is written in
I=i+1; 20 I=i+1; 20
columns after optimization
} }
Total 50 41
Induction variable elimination
Another important optimization is to find induction variables in loops and optimize their
computation.
66
Two variables say x, y are said to be inductive if constant change in x produce same constant
change in y.
Before optimization After optimization
x=5; X= 5 ; Now in above example if x increased by 1, then value
While ( x > 0 ) y = 20 ;
{ y=4 * x; While ( x > 0 ) of y is decreased by 4 that’s why x, y are inductive
x= x+1; { y= y – 4 ; variables
} x = x + 1 ;}
Peephole optimization
A simple but effective technique for locally improving the target code is peephole optimization,
which is done by examining a sliding window of target instructions (called the peephole) and replacing
instruction sequences within the peephole by a shorter or faster sequence, whenever possible. Peephole
optimization can also be applied directly after intermediate code generation to improve the intermediate
representation. The peephole is a small, sliding window on a program. The code in the peephole need not
be contiguous, although some implementations do require this.
Following are the examples of peephole optimizations:
Eliminating Redundant Loads and Stores
Unreachable code
Flow-of-control optimizations
Algebraic simplifications
Use of machine idioms
67
Flow-of-Control Optimizations
Simple intermediate code-generation algorithms frequently produce jumps to jumps, jumps to
conditional jumps, or conditional jumps to jumps. These unnecessary jumps can be eliminated in either
the intermediate code or the target code by the following types of peephole optimizations.
We can replace the sequence by the sequence
goto L1 goto L2
... …
Ll: goto L2 L1 : goto L2
If there are now no jumps to L1, then it may be possible to eliminate the statement L1: goto L2
provided it is preceded by an unconditional jump.
Before optimization After optimization
if ( a < b ) goto L1 if ( a < b ) goto L2
…………. ………….
L1 : goto L2
Algebraic Simplification
Consider the statement x =x+0;
Here we adding 0 to x which means add nothing but it require 2 unit time for execution 1for addition and
1 for assignment of result to x
This instruction can be replaced by only x because it results same
Another example is x = x* 1;
68