0% found this document useful (0 votes)
98 views68 pages

Compiler Construction Notes

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
98 views68 pages

Compiler Construction Notes

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Name- Prof A.

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.

2. Lexical Analysis: (5)


Role of a Lexical analyzer, input buffering, specification and recognition of tokens, finite
automata implications, designing a lexical analyzer generator.

3. Syntax Analysis: (6)


Role of Parser, Writing grammars for context free environments, Top-down parsing, Recursive
descent and predictive parsers (LL), Bottom-Up parsing, Operator precedence parsing, LR, SLR
and LALR parsers

4. Syntax Directed Translation: (6)


Syntax directed definitions, construction of syntax tree, Bottom-up evaluation of S-attributed
definitions, L-attributed definitions, Top-down translation and Bottom-up evaluation of
inherited attributes, analysis of syntax directed definitions.

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.

6. Intermediate Code Generation: (3)


Intermediate languages, declarations, assignment statements and Boolean expressions, case
statements, back patching, procedure calls.

7. Code Generation: (5)


Issues in design of a code generator and target machine, Run time storage management, Basic
blocks and flow graphs, Next use information and simple code generator, Issues of register
allocation, assignment and basic blocks, code generation from Dags and the dynamic code
generation algorithm.

8. Code Optimization: (5)


Sources of optimization, Peephole optimization and basic blocks, loops in flow graphs, Data flow
analysis and equations, code improving transformation and aliases, Data flow analysis and
algorithms, symbolic debugging of optimized code.

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

Programming languages are notations for describing computations to people and to


machines. The world as we know it depends on programming languages, because all the
software running on all the computers was written in some Programming language. But, before a
program can be run, it first must be translated into a form in which it can be executed by a
computer. The software systems that do this translation are called compilers.
A compiler is a program that can read a program in one language- the source language -
and translate it into an equivalent program in another language - the target language;
An important role of the compiler is to report any errors in the source program that it
detects during the translation process
Source Target
program
Compiler
program

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

An interpreter is another common kind of language processor. Instead of producing a


target program as a translation, an interpreter appears to directly execute the operations
specified in the source program on inputs supplied by the user

Fetch Next Instruction

Source Program Output


Interpreter Decode Instruction into machine
Input language

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

Modified syntax tree

Symbol table Intermediate code generation


Error table

Intermediate representation

Machine independent optimization

Optimized intermediate representation

Cede generation

Target code

Machine dependent optimization

Optimized target code


Translation of an assignment statement
position=initial + rate * 60.00
Lexical Analyzer

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.

4. Intermediate Code Generation


In this phase intermediate code is generated using parse tree. This intermediate
representation should have two important properties:
1. It should be easy to produce &
2. It should be easy to translate into the target machine.
We consider an intermediate form called three-address code, which consists of a sequence of
assembly-like instructions with three operands per instruction. Each operand can act like a
register.

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

Terms for Parts of Strings


The following string-related terms are commonly used:
1. A prefix of string s is any string obtained by removing zero or more symbols from the end of s.
For example, ban, banana, and E are prefixes of banana.
2. A suffix of string s is any string obtained by removing zero or more symbols from the
beginning of s. For example, nana, banana, and E are suffixes of banana.
3. A substring of s is obtained by deleting any prefix and any suffix from s. For instance, banana,
nan, and E are substrings of banana.
4. The proper prefixes, suffixes, and substrings of a string s are those, prefixes, suffixes, and
substrings, respectively, of s that are not E or not equal to s itself.
5. A subsequence of s is any string formed by deleting zero or more not necessarily consecutive
positions of s. For example, baan is a subsequence of banana
One pass compiler
In computer programming, a one-pass compiler is a compiler that passes through the
source code of each compilation unit only once. In other words, a one-pass compiler does not
"look back" at code it previously processed.
Another term sometimes used is narrow compiler, which emphasizes the limited scope a
one-pass compiler is obliged to use. This is in contrast to a multi-pass compiler which traverses
the source code and/or the abstract syntax tree several times, building one or more intermediate
representations that can be arbitrarily refined.

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.

C code Intermediate Code Intermediate Code


pass I (Forward Referencing) pass II (Back patching)
j = 0; LABEL INSTRUCTIONS
while (j < 10) 100 j =0 100 j =0
{ printf(“%d”,j); LOOP 101 if (j<10)then goto BODY 101 if (j<10)then goto 103
j = j + 1;
102 goto LAST 102 goto 106
}
label address BODY 103 out j 103 out j
LOOP 101 104 j=j+1 104 j=j+1
BODY 103
LAST 106 105 goto LOOP 105 goto 101
Intermediate Table LAST 106 Exit 106 Exit

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

Task of lexical analyzer


1) The lexical analyzer is the part of the compiler that reads the source text; it may perform
certain other tasks besides identification of lexemes. That is token generation.
2) Stripping out (removal of) comments and white space (blank, new line, tab, and perhaps
other characters that are used to separate tokens in the input).
3) Correlating error messages generated by the compiler with the source program. For
instance, the lexical analyzer may keep track of the number of new line characters seen, so
it can associate a line number with each error message.
4) Preparation of symbol table

Sometimes, lexical analyzers are divided into a cascade of two processes:


1) Scanning consists of the simple processes that do not require tokenization of the input,
such as deletion of comments and compaction of consecutive white space characters into one.
2) Lexical analysis proper is the more complex portion, where the scanner produces the
sequence of tokens as output.

Why lexical analysis is separate stage in complier?


1) Simplicity of design is the most important consideration. If we are designing a new
language, separating lexical and syntactic concerns can lead to a cleaner overall language design.
2) Compiler efficiency is improved. A separate lexical analyzer allows us to apply
specialized techniques that serve only the lexical task, not the job of parsing. In addition,
specialized buffering techniques for reading input characters can speed up the compiler
significantly.
3) Compiler portability is enhanced. Input-device-specific peculiarities can be restricted to
the lexical analyzer.

Token is a pair consisting of a token name and an optional attribute value.

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

Token Informal description Simple lexemes


if (keyword) Characters i, f if
else (keyword) Characters e, l ,s ,e Else
comparison (operator) < or<=or>or>=or==or!= <=, !=
id (identifier) Letter followed by letter or digit Pi, score, I, j, k
number (numeric constant) Any numeric constant 3.14159, 0, 7
literal (character constants) Anything but “ surrounded by “ ‘s “string”, “HELLO”

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

Integer floating point character string


Ex:- 5 3.14 ‘c’ “computer”

Attributes for Tokens

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

Design and implementation of lexical analyzer


Deign of lexical analyzer can be explained by finite automata.
A finite automaton, which recognizes an identifier, is shown in following figure. We know
all identifier starts with letter followed by letter or digit. The delimiter identifies the token as
identifier.
We can write code for each state in transition diagram as fallows for that purpose we
require following standard functions
 getchar ( )
It returns the input character pointed by lookahead pointer and advances the lookahead
pointer to next character.
 retract ( )
Used to retract lookahead pointer one character.
To understand this function, consider the above transition diagram. Token
identifier can be recognized only after reading delimiter. But delimiter is not part of token.
So it should not be stored with the token in the table.
[we use * to indicate state on which input retraction takes place ]
 install ( )
If token is not in he symbol table then we can add it using install ( ).
 error ( )
To display error message.
Design & Implementation of lexical Analyzer for Identifier

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.

Types of parse tree


Parser

Top Down parser Bottom Up parser

Recursive Predictive Operator LR parser Shift Reduce


Descent Parser Parser precedence Parser Parser Parser

Simple LR Canonical LR LALR


Parser Parser Parser

Common programming errors can occur at many different levels.

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.

2. int a[5], b[5], c[5];


c = a + b; //not valid because datatype of a, b, c is integer array for manipulating array
. Programmer should specify index

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.

Following are some error recovery strategies:

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

Immediate left recursion


If any non-terminal having production of the form AAα | β. Immediate left recursion can be
eliminated by interfusing new non-terminal A’ and modifies the grammar as follows
A βA’ S
A’ αA’ | €
A β
Now we can expand the rule
AAα1 | Aα2 | . . . | Aαm | β1| β2| . . . | βn can be modified as S β
A β1A’| β2A'| . . . | βnA’
Aα1A’ | α2A’ | . . . | αmA’ | € A β
:
General left recursion Algorithm Eliminating left recursion.
INPUT: Grammar G with no cycles or €-productions.
If production of the
OUTPUT: An equivalent grammar with no left recursion.
form METHOD: Apply the algorithm to G. Note that the resulting non-left-
recursive grammar may have €-productions.
SAβ
Arrange the nonterminals in some order A1, A2, . . . , Am.
ASβ | € for ( each i from 1 to n )
{ for ( each j from 1 to i - 1 )
Here S is left recursive, because
{ replace each production of the form Ai  Ajγ
S  Aβ  Sββ. by the productions Ai  δ1γ | δ2γ | . . . | δkγ
This general left recursion can where Aj  δ1 | δ2 | . . . | δk are all current Aj-productions
}
be eliminated by following eliminate the immediate left recursion among the Ai-productions
algorithm. }

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
ScAd
Aab | 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 AX1X2…..Xk
18
void A()
{
Choose an A-production, AX1X2…..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.

FIRST and FOLLOW


The construction of both top-down and bottom-up parsers is aided by two functions, FIRST and
FOLLOW, associated with a grammar G. During top-down parsing, FIRST and FOLLOW allow us to
choose which production to apply, based on the next input symbol. During panic-mode error recovery,
sets of tokens produced by FOLLOW can be used as synchronizing tokens.
Define FIRST(α), where α is any string of grammar symbols, to be the set of terminals that begin
strings derived from α. If A€, then € is also in FIRST(α).
To compute FIRST(X) for all grammar symbols X, apply the following rules
Until no more terminals or €: can be added to any FIRST set.
1. If X is terminal, then FIRST(X) is {X}.
2. If X  ε is a production, then add ε to FIRST(X).
3. If X is nonterminal and X Y1 Y2 ... Yk. is a production, then FIRST(X) =FIRST(Y1),
If FIRST(Y1) contain ε then we add FIRST(Y2) and so on.

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

Predictive parser is tabular implementation of recursive descent parser.

Predictive parser consist of

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.

 If A = a = $, parser halts and output is accepted.


 If A = a ≠ $ then parser pops A and advances input pointer to next input symbol.
 If A is non terminal, parser checks parsing table entry A[A, a]. This entry is either an A-production
or an error. If it is A-production AXYZ then parser replace A by ZYX (X is at top of stack)
Algorithm : Construction a predictive parsing table.

INPUT : grammar G

OUTPUT : Parsing table M

METHOD for each production A  α of grammar G, do following entry.

1. For each terminal “a” in FIRST(A) add A  α to M[A, a].


2. If € is in FIRST(α), then for each terminal “b” in FALLOW(A), add A  α to M[A, b].
3. Mark all undefined entries as error.

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.

set ip to point to the first symbol of w;

set X to the top stack symbol;

while ( X ≠ $ )

{ /* stack is not empty */

if ( X is a ) pop the stack and advance ip;

else if ( X is a terminal ) error();

else if ( M[X, a] is an error entry ) error();

else if ( M[X,a] = X  Y1Y2…. Yk )

output the production X  YlY2 …. Yk;

pop the stack;

push Yk, Yk-1,. . . , Yl onto the stack, with Yl on top;

set X to the top stack symbol;

Example
SAB |€
AaAB|€
B bA
Consider string “aaabb”(valid) Consider string “abba”(invalid)
Stack Input Action Stack Input Action
$S Ababb$ SAB $S abba$ SAB
$BA Ababb$ AaAB $BA abba$ AaAB
$BBAa Ababb$ Pop a & ADVANCE $BBAa abba$ Pop a & ADVANCE
$BBA babb$ A€ $BBA bba$ A€
$BB babb$ BbA $BB bba$ BbA
$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$ BbA
$BBA bb$ A€ $Ab ba$ Pop b & ADVANCE
$BB bb$ B bA $A a$ AaAB
$BAb bb$ Pop b & ADVANCE $BAb a$ Pop a & ADVANCE
$BA b$ A€ $BA $ A€
$B b$ BbA $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.

Shift reduce parser


This is bottom up parsing method
It uses stack to take actions
Actions are
Shift: - shifting the input symbol on to stack.
Reduce: - If top of stack contains RHS of any production it can be replaced by non-terminal on LHS side .
Thus shift reduce parser is successful in reducing input string to start symbol then it outputs
accepted by grammar.
We use $ to mark the bottom of the stack and also the right end of the input. Conventionally,
when discussing bottom-up parsing, we show the top of the stack on the right, rather than on the left as
we did for top-down parsing.
Initially, the stack is empty, and the string w is on the input, as follows:

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
SaAb Right sentential form Handle Reducing production
AaA|Bb Aabbb B B b
Bb aaBbb Bb ABb
aaAb aA AaA
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 SaAb
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.

LR parsing is attractive for a variety of reasons:


 LR parsers can be constructed to recognize virtually all programming language constructs for
which context-free grammars can be written. Non-LR context-free grammars exist, but these can
generally be avoided for typical programming-language constructs.
 The LR-parsing method is the most general non-backtracking shift-reduce parsing method known,
yet it can be implemented as efficiently as other, more primitive shift-reduce methods
 An LR parser can detect a syntactic error as soon as it is possible to do so on a left-to-right scan of
the input.
 The class of grammars that can be parsed using LR methods is a proper superset of the class of
grammars that can be parsed with predictive or LL methods.
For a grammar to be LR(k), we must be able to recognize the occurrence of the right side of a
production in a right-sentential form, with k input symbols of lookahead. This requirement is far less
stringent than that for LL(k) grammars where we must be able to recognize the use of a production
seeing only the first k symbols of what its right side derives. Thus, it should not be surprising that LR
grammars can describe more languages than LL grammars.

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.

The LR-Parsing Algorithm

A schematic of an LR parser is shown


below.
It consists of Input a1 … ai … an $
 an input,
 an output,
 a stack,
 a driver program, and stack X Predictive Output
 a parsing table that has two parts parsing
(ACTIONa nd GOTO). program
The driver program is the same for all LR parsers; Y
only the parsing table changes from one parser to
another. The parsing program reads characters ACTION GOTO
from an input buffer one at a time. Where a shift- Z
reduce parser would shift a symbol, an LR parser
shifts a state. Each state summarizes the Model of LR Parser
information contained in the stack below it
$

Difference Between SLR , Canonical LR, LALR


SLR Canonical LR LALR
1 Set of LR(0) items are generated Set of LR(1) items are generated Set of LR(0) items are
generated
2 Number of items are less than Number of items are more than LALR is manager of LR(1)
LR(1) but nearly equal to LALR LR(1) and LALR Number of items are less than
LR(1)
3 To construct parsing table To construct parsing table FIRST To construct parsing table
FALLOW function is used function is used FIRST function is used
4 Shift reduce conflict is occurs Rarely shift reduce conflict is Reduce-reduce conflict is
occurs occurs
5 Good method Better method Most powerful method
6 Memory requirement is less Memory requirement is large Memory requirement is less
Operator precedence parser
1. Precedence Relations
Bottom-up parsers for a large class of context-free grammars can be easily developed using operator
grammars.
Operator grammars have the property that no production right side is empty or has two adjacent non-
terminals. This property enables the implementation of efficient operator-precedence parsers. These
parser rely on the following three precedence relations:

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.

Inherited and Synthesized Attributes


We shall deal with two kinds of attributes for non-terminals:
 The value of synthesized attribute at a node is computed the value of attribute at child of
that node in the parse tree.
Example EE1 + T
Semantic rule [Link]  [Link] + [Link]
(val is synthesized attribute associated with symbols)
 The value of inherited attribute is computed from the value of siblings and parent of that
node in the parse tree.
Terminals can have synthesized attributes, but not inherited attributes. Attributes for terminals
have lexical values that are supplied by the lexical analyzer; there are no semantic rules in the
Syntax Directed Definition itself for computing the value of an attribute for a terminal.

Annotated parse tree


A parse tree, with values of its attributes at each node is called annotated parse tree
27
Evaluation of syntax Directed Definition
 Construct annotated parse tree
 Attributes at node of a pares tree are evaluated
 Evaluation order is depend upon whether the attribute is synthesize or inherited
 In synthesized attribute, we evaluate in bottom up order (post-order traversal) i.e.
children first.

In inherited attribute, we evaluate in top down order (pre-order traversal) i.e. parent first.

The Syntax Directed Definition having both synthesized & inherited attributes, there is no
guarantee of any order to evaluate an attributes at node
Example
 Synthesized attribute Consider grammar of expression

Grammar of Production Semantic rules Construct annotated parse tree for


expression input string 5 + 3 * 4
LE LE [Link] = [Link] OR [Link] = 17
EE+T|T EE+T Print([Link])
TT*F|F ET [Link] = [Link] + [Link] [Link] = 17 return
F  ( E ) | TT*F [Link] = [Link]
[Link] = 5 + [Link] = 12
digit TF [Link] = [Link] + [Link]
F(E) [Link] = [Link] [Link] = 5 [Link] = 3 * [Link] = 12
F  digit [Link] = [Link]
[Link] = [Link] [Link] = 5 [Link] = 3 [Link] = 4

[Link] = 5 [Link] = 3

 Inherited attribute Consider grammar of declaration

grammar of Production Semantic rules Construct annotated parse tree for


declaration declaration
int a , b , c ;
D  TL D  TL [Link] = [Link] D
T int|double T  int [Link] = int
L  L, id | id T double [Link] = double [Link] = int [Link] = int
L  L, id [Link] = [Link]
int [Link] = int , c
L  id [Link] = [Link] ,
[Link]
[Link] = int , b
[Link] = [Link]
a

Evaluation orders for Syntax Directed Definition


Evaluation order can be determined using “Depending Graph”. Dependency graphs are a useful
tool for determining an evaluation order for the attribute instances in a given parse tree. While

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:

Production Semantic rule


EE+T [Link]  [Link] + [Link]

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 AX1X2 … 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

Semantic Rules with Controlled Side Effects


In practice, translations involve side effects:
 a desk calculator might print a result;
 a code generator might enter the type of an identifier into a symbol table.
 Update global variable

Ordering evaluation of semantic rules


1. Parse tree method
2. Rule-based method
3. Oblivious method
Parse tree method

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

Applications of Syntax-Directed Translation


The syntax-directed translation techniques will be applied in type checking and
intermediate-code generation.
The main application is construction of syntax trees. Since some compilers use syntax
trees as an intermediate representation, a common form of SDD turns its input string into a tree.
To complete the translation to intermediate code, the compiler may then walk the syntax tree,
using another set of rules that are in effect an SDD on the syntax tree rather than the parse
tree.
We consider two Syntax Directed Definition's for constructing syntax trees for expressions.
 The first, an S-attributed definition, is suitable for use during bottom-up parsing.
 The second, L-attributed, is suitable for use during top-down parsing.
 The final example of this section is an L-attributed definition that deals with basic and
array types.

Construction of Syntax Trees


Each node in a syntax tree represents a construct; the children of the node represent the
meaningful components of the construct.
A syntax-tree node representing an expression El + E2 has label + and two children
representing the sub-expressions El and E2.
We shall implement the nodes of a syntax tree by objects with a suitable number of fields.
Each object will have an op field that is the label of the node.
31
The objects will have additional fields as follows:
1. If the node is a leaf, an additional field holds the lexical value for the leaf.
 A function Leaf (id, entry) creates a leaf node for identifier. Field entry containing
pointer to symbol table entry for identifier.
 A function Leaf (num, val) create a leaf node for constant. Field val containing
value of the number.
If nodes are viewed as records, then Leaf returns a pointer to a new record for a
leaf.
2. If the node is an interior node, there are as many additional fields as the node has children
in the syntax tree.
 A constructor function Node takes two or more arguments:
Node(op, arg1, arg1, . . . , argk) creates an object with first field op and k additional fields
on which operation is to be performed.

Production Semantic rule a*b+c


E  E1 + T3 [Link] = new node(+, [Link], [Link])
E1  T1 * T2 [Link] = new node(*, [Link], [Link])
T1  Id, a [Link] = new leaf(id, [Link])
T2  Id, b [Link] = new leaf(id, [Link])
T3  Id, c [Link] = new leaf(id, [Link])

[Link]

[Link] [Link]
* [Link] + [Link]
+

* id

Ptr to entry for c


id id

Ptr o entry for a Ptr o entry for b

Syntax-Directed Translation Schemes


Syntax-directed translation schemes are a complementary notation to syntax directed
definitions. All of the applications of syntax-directed definitions in can be implemented using
syntax-directed translation schemes.
A syntax-directed translation scheme (SDT) is a context free grammar with program
fragments embedded within production bodies. The program fragments are called semantic
actions and can appear at any position within a production body. By convention, we place curly
braces around actions; if braces are needed as grammar symbols, then we quote them.

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.

Postfix Translation Schemes


The simplest SDD implementation occurs when we can parse the grammar bottom-up and
the SDD is S-attributed. In that case, we can construct an SDT in which each action is placed at
the end of the production and is executed along with the reduction of the body to the head of
that production. SDT's with all actions at the right ends of the production bodies are called
postfix SDT's.
Example
The postfix SDT in below figure implements the desk calculator. SDD figure below, with
one change: the action for the first production prints a value. The remaining actions are exact
counterparts of the semantic rules. Since the underlying grammar is LR, and the SDD is S-
attributed, these actions can be correctly performed along with the reduction steps of the parser.
LEn { print([Link]); )
E  E1+T { [Link]= [Link]+ [Link]; }
ET { [Link]= [Link]; }
T  T1 * F { [Link] = TI .val x [Link]; )
TF { [Link] = [Link]; }
F(E) {[Link]=[Link];}
F  digit { [Link] = digit.lexva1; }
Postfix SDT implementing the desk calculator

Parser-Stack Implementation of Postfix SDT's


Postfix SDT's can be implemented during LR parsing by executing the actions when
reductions occur. The attribute(s) of each grammar symbol can be put on the stack in a place
where they can be found during the reduction. The best plan is to place the attributes along with
the grammar symbols (or the LR states that represent these symbols) in records on the stack
itself. Bottom up parser uses stack to hold information about sub-trees that have been parsed.
The stack have extra field val to hold values synthesized attributes.
Below figure shows the parser stack contains records with a field for a grammar symbols
and a field for symbol values.
The stack implements by a pair of array state and val.

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

Consider the production X ABC


The semantic rules associated with above production
X.x = f(A.a, B.b, C.c)
Where A.a is attribute of A
Where B.b is attribute of B
Where C.c is attribute of C
Before reduction of ABC to X, the value of the attribute C.C to val[top], B.b to val[top-1],
and so on.

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

General 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

If successful, malloc() returns a pointer to the block of memory allocated. Otherwise, it


returns a NULL pointer. One must always check to see if the pointer returned is NULL. If
malloc() is successful, objects in dynamically allocated memory can be accessed indirectly by
dereferencing the pointer, appropriately cast to the type of pointer required.
The size of the memory to be allocated must be specified, in bytes, as an argument to malloc().
Since the memory required for different objects is implementation dependent, the best way to
specify the size is to use the sizeof operator. Recall that the sizeof operator returns the size, in
bytes, of the operand.
For example, if the program requires memory allocation for an integer, then the size
argument to malloc() would be sizeof(int). However, in order for the pointer to access an integer
object, the pointer returned by malloc() must be cast to an int *. The code takes the following
form:
int *ptr;
ptr = (int *)malloc(sizeof(int));
Now, if the pointer returned by malloc() is not NULL, we can make use of it to access the
memory indirectly. For example:
if (ptr != NULL)
*ptr = 23;
printf("Value stored is %d\n", *ptr);
Later, memory allocated above may no longer be needed. In which case, it is important to free
the memory.
free(ptr); //deallocates the previously allocated block of memory pointed to by ptr.

Run time sub-division


We conclude our discussion of storage class and scope by briefly describing how the
memory of the computer is organized for a running program. When a program is loaded into
memory, it is organized into three areas of memory, called segments:

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);

Parameter passing techniques


1 By value 2 By reference 3 By name 4 By result 5 By value-result
1. By value
For by-value parameter passing, the formal parameter is just like a local variable in the
activation record of the called method, with one important difference: it is initialized using the
value of the corresponding actual parameter, before the called method begins executing.
 When parameters are passed by value, changes to a formal do not affect the actual
2. By reference
For passing parameters by reference, the lvalue of the actual parameter is computed
before the called method executes. Inside the called method, that lvalue is used as the lvalue of
the corresponding formal parameter. In effect, the formal parameter is an alias for the actual
parameter—another name for the same memory location.
 Most efficient for large objects
3. By name
For passing parameters by name, each actual parameter is evaluated in the caller’s context, on
every use of the corresponding formal parameter.
 The actual parameter is treated like a little anonymous function
 Whenever the called method needs the value of the formal (either rvalue or lvalue) it calls
the function to get it
 The function must be passed with its nesting link, so it can be evaluated in the caller’s
context
4. By result
For by-result parameter passing, the formal parameter is just like a local variable in the
activation record of the called method—it is uninitialized. After the called method finished
executing, final value of the formal parameter is assigned to the corresponding actual parameter.
 Also called copy-out
 Actual must have an lvalue
 Introduced in Algol 68; sometimes used for Ada
5. By value-result
For passing parameters by value-result, the formal parameter is just like a local variable in
the activation record of the called method. It is initialized using the value of the corresponding
actual parameter, before the called method begins executing. Then, after the Called method
finishes executing; the final value of the formal parameter is assigned to the actual parameter.
 Also called copy-in/copy-out
 Actual must have an lvalue
Symbol table
A compiler uses a symbol table to keep track of scope and binding information about names.
The symbol table searches every time a name is encountered in the source text. Changes to
symbol occur if new information about an existing name discovered.
Symbol table mechanisms must allow us to add new entries and fine existing entries.

Symbol table entries


Each entry in the symbol table is for the declaration of the name. the format for entries
must have uniform format. Each entry can be implemented as record consisting sequence of
attributes about name. to keep symbol entry uniform , it may be convenient for some information

43
about a name to be kept outside the table entry, with only pointer to this information stored in
the record.

In the case of variable names attributes may provide information


1. About the storage allocated for a name (on stack / static or dynamic data section)
2. Its data type,
3. Its scope (where in the program its value may be used),

In the case of procedure names attributes may provide information


4. The number and types of its arguments, (parameters)
5. The method of passing each argument (for example, by value or by reference), and
6. The type returned.
Symbol table also contains

 Macro expansion table


 Switch table

Symbol table preparation mechanism


The two symbol table mechanisms are

 linear lists and


 hash tables.

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

 Lookup : find most recently created entry


 Insert : to make new entry
 Delete : to delete old, unused entry

List data structure for symbol table


It is simplest & easiest data structure data structure for implementating symbol table.
Arrays are used to store name and associated information. New information is added to list in
the order in which they are encountered. There is one pointer which pointing to next available
slot where we can store new information, more generally it is next to last record stored in table
If we reaches to the beginning to the array without finding the name, complier generate error.
If symbol table contain n records, the time complexity for insert new record in symbol table is
0(n) because first we have to check it is already present in symbol table or not.
Example symbol table as list
int j; Sr no Name datatype Scope Line no
float f; 1 j int Local to main 8

44
double d; 2 ch char Local to display 20
char ch; 3 f folat Global 5
4 d double Global 6

Hash table data structure for symbol table


Variation in the searching techniques in known as hashing. Open hashing refers to the
property that there is no limit on the number of entries that can be in the table.
In the basic hashing scheme, there are two parts to the data structure
1. A hash table consisting of a fixed array of m pointers to table entries
2. Table is organized into m separate linked lists, called buckets. Each record in the
symbol table appears on exactly one of these lists.
To determine whether there is an entry for name s in the symbol table, apply a hash
function h( ) to s, such that h(s) returns an integer between 0 to m-1. if s in symbol table
then it is present of list h(s). if s is not present then first list is created & record is inserted
at front of newly created list.
For searching old name say x in symbol table then first apply function h(x) it will
given us bucket number. Then find that x in list pointed by that bucket.
Generally number of buckets must be prime.
Example symbol table as hash table
int i, h;
float f, t, p; 1 (int) i h
double d;
char x, c; 1 (float) f t p

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.

This intermediate representation should have two important properties:

 It should be easy to produce and


 It should be easy to translate into the target machine.

Static Intermediate Intermediate


Parser Checker code Code
Representation Generator
generation
code
Postfix notations
Operators are used with operands to build expressions
Postfix notations are liberalized representation of a syntax tree.
Consider example a = b * - c + b * - c
Bracketization ( a = ( ( b * ( - c ) ) + ( b * ( - c ) ) ) )
Postfix notation a b c - * b c - * + =

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

Array representation Syntax tree

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

Semantic productions for syntax tree


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 + E4 [Link] = mknode (“+” , [Link] , [Link])
E3  E5 * E6 [Link] = mknode (“*” , [Link] , [Link])
E4  E8 * E9 [Link] = mknode (“*” , [Link] , [Link])
E5  id ( b ) [Link] = mkleaf ( id , pointer to symbol table entry for b )
E6  - E7 [Link] = mknode (“-” , [Link] )
E7  id ( c ) [Link] = mkleaf ( id , pointer to symbol table entry for c )
E8  id ( b ) [Link] = mkleaf ( id , pointer to symbol table entry for b )
E9  - E10 [Link] = mknode (“=” , [Link] )
E10  id ( c ) [Link] = mkleaf ( id , pointer to symbol table entry for 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

Types of three address code

1. Assignment instructions of the form x = y op z, where op is a binary arithmetic or logical operation,


and x, y, and z are addresses.

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.

3. Copy instructions of the form x = y, where x is assigned the value of y.

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,

Implementations of three address code


It is one of the forms of an intermediate code called three-address code, which consists of a
sequence of assembly-like instructions with three operands per instruction. Each operand can act like a
register. There are several points to be remembered while generating three-address instructions.

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
EEORE | E AND E | NOT E | (E) | E relop E | id | true | false

Flow of control statement


Here we will see the translation of Boolean expression into three address code. The control statements are
 if ….then …..else
S  if E then Statement | if E then Statement1 else Statement2
 while…..do
S  while E do Statement
Three address code for if…else
Sr. Code Line Label Instructions
no no
1 if ( a < b) 1 if a < b then goto L1
2 { Print(“small a”); 2 Out “small b”;
3 } 3 Goto L2
4 Else 4 L1 Out “small a”;
5 { Print(“small b”); 5 L2 Exit
6 }
Three address code for switch case
Sr. Code Line Label Instructions
no no
1 Switch ( ch ) 1 if ch == 1 then goto L1
2 { 2 if ch == 2 then goto L2
3 Case 1 : c =a + b ; break ; 3 if ch == 3 then goto L3
4 Case 2 : c =a – b ; break ; 4 if ch == 4 then goto L4
5 Case 3 : c =a * b ; break ; 5 L1 c=a+b;
6 Case 4 : c =a / b ; break ; 6 goto LAST
7 default : break ; 7 L2 c=a-b;
8 } 8 goto LAST
9 L3 c=a*b;
10 goto LAST
11 L4 c=a/b;
12 LAST Exit

Three address code for while loop


[Link] Code Line Label Instructions
no
1 while ( a < 10 ) 1 LOOP if a < 10 then goto BODY
2 { 2 goto LAST
3 Printf ( “a” ) ; 3 BODY Out “ a ”;
4 a=a+1; 4 a=a+1;
5 } 5 goto LOOP
6 LAST Exit
50
Addressing Array Elements
Array elements can be accessed quickly if they are stored in a block of consecutive locations. In C
and Java, array elements are numbered 0, 1, . . . , n - 1, for an array with n elements. If the width of each
array element is w, then the ith element of array A begins in location base + i * w where base is the relative
address of the storage allocated for the array. That is, base is the relative address of A[0].
In two dimensions, we write A[i1][i2] in C and Java for element i2 in row il . Let wl be the width of
a row and let w2 be the width of an element in a row. The relative address of A[il] [i2] can then be
calculated by the formula base + il * wl + i2 * w2
In k dimensions, the formula is base + il * wl + i2 * w2 + …+ ik * wk … (where wj, for 1 <= j <= k)
Alternatively, the relative address of an array reference can be calculated in terms of the numbers of
elements nj along dimension j of the array and the width w = wk of a single element of the array. In two
dimensions (i.e., k = 2 and w = w2), the location for A[il] [i2] is given by base + (il x n2 + iq) x w (6.5)
More generally, array elements need not be numbered starting at 0. In a one-dimensional array,
the array elements are numbered low, low + 1, . . . , high and base is the relative address of A[low].
Expressions above can be both be rewritten as i x w + c, where sub expression c = base - low * w
can be pre-calculated at compile time.
Note that c = base when low is 0. We assume that c is saved in the symbol table entry for A, so the
relative address of A[i] is obtained by simply adding i x w to c
Compile-time pre-calculation can also be applied to address calculations for elements of
multidimensional arrays; however there is one situation where we cannot use compile-time pre-
calculation: when the array's size is dynamic. If we do not know the values of low and high (or their
generalizations in many dimensions) at compile time, then we cannot compute constants such as c. Then,
formulas must be evaluated as they are written, when the program executes. The above address
calculations are based on row-major layout for arrays, which is used in C and Java. A two-dimensional
array is normally stored in one of two forms, either row-major (row-by-row) or column-major (column-by
column). Below figure shows the layout of a 2 x 3 array A in (a) row-major form and (b) column-major
form. Column-major form is used in the Fortran family of languages.
A[1,1] A[1,1] First [ ]
First A[1,2] A[2,1] column
row A[1,3] A[1,2] Second 2 []
A[2,1] A[2,2] column
Secon A[2,2] A[1,3] Third
d row A[2,3] A[2,3] column 3 int
Row major Column major Type expression for int [2] [3]
representation representation

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.

Consider three address code for if…else


[Link] Code Line Label Instructions
no
1 If ( a < b) 1 if a < b then goto L1
2 { 2 Out “small b”;
3 Print(“small a”); 3 Goto L2
4 } 4 L1 Out “small a”;
5 Else 5 L2 Exit
6 {
7 Print(“small b”);
8 }

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.

Intermediate table Modified instructions


Label Address Line no Instructions
L1 4 1 if a < b then goto 4
L2 5 2 Out “small b”;
3 Goto 5
4 Out “small a”;
5 Exit

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.

Source Front Intermediate Code Target


program code generator program
end
Position of code generator

A code generator has three primary tasks:


[Link] selection involves choosing appropriate target-machine instructions to implement
the IR statements.
[Link] allocation and assignment involves deciding what values to keep in which registers.
[Link] ordering involves deciding in what order to schedule the execution of instructions.

ISSUES IN THE DESIGN OF A CODE GENERATOR


Input to the Code Generator
The input to the code generator is the intermediate representation of the source program
produced by the front end, along with information in the symbol table that is used to determine
the run-time addresses of the data objects denoted by the names in the IR.
The many choices for the IR include
 Three-address representations such as quadruples, triples, indirect triples;
 Virtual machine representations such as byte-codes and stack-machine code;
 Linear representations such as postfix notation;
 Graphical representations such as syntax trees and DAG's.
We assume that the front end has scanned, parsed, and translated the source program into
a relatively low-level IR, so that the values of the names appearing in the IR can be represented
by quantities that the target machine can directly manipulate, such as integers and floating-point
numbers.

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:

LOD AX , a INCR a We need to know instruction costs in order to design good


ADD AX , 1 code sequences but, unfortunately, accurate cost information is
STO a often difficult to obtain. Deciding which machine-code sequence is
best for a given three-address construct may also require knowledge
about the context in which that construct appears.
Register Allocation
A key problem in code generation is deciding what values to hold in what registers.
Registers are the fastest computational unit on the target machine, but we usually do not have
enough of them to hold all values. Values not held in registers need to reside in memory.
Instructions involving register operands are invariably shorter and faster than those involving
operands in memory, so efficient utilization of registers is particularly important.
The use of registers is often subdivided into two sub-problems:
1. Register allocation, during which we select the set of variables that will reside in registers at
each point in the program.
2. Register assignment, during which we pick the specific register that a variable will reside in.
Finding an optimal assignment of registers to variables is difficult, even with single-register
machines. Mathematically, the problem is NP-complete. The problem is further complicated
because the hardware and/or the operating system of the target machine may require that
certain register-usage conventions be observed.
Example certain machines require register-pairs (an even and next odd numbered register) for
some operands and results. For example, on some machines, integer multiplication and integer
division involve register pairs.
MULT X, Y :-The multiplication instruction is of the form where x, the multiplicand, is the
even register of an even/odd register pair and y, the multiplier, is the odd register. The product
occupies the entire even/odd register pair.
DIV X, Y :-The division instruction is of the form where the dividend occupies an
even/odd register pair whose even register is x; the divisor is y. After division, the even register
holds the remainder and the odd register the quotient.
consider the three-address code sequences and the optimal assembly-code sequences
Three address code Optimal assembly code sequence
LOD AX , a
t := a + b; ADD AX , b
t := t * c; MULT AX , d
t := t / d; DIV AX , d
STO t

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 Target Machine

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

 RISC (reduced instruction set computer),


 CISC (complex instruction set computer), and
 Stack based.

 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.

We assume the following kinds of instructions are available:


 Load operations: (LOD dst, addr) loads the value in location addr into location dst.
This instruction denotes the assignment dst = addr.

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.

We assume our target machine has a variety of addressing modes:


1. Direct addressing mode
2. Indirect addressing mode
3. Register addressing mode
4. Implied addressing mode

 Program and Instruction Costs


We often associate a cost with compiling and running a program. Some common cost measures
are
 the length of compilation time
 the size of target program
 Running time and
 power consumption of the target program.
Determining the actual cost of compiling and running a program is a complex problem. We shall
assume each target-language instruction has an associated cost. For simplicity, we take the cost
of an instruction to be one plus the costs associated with the addressing modes of the
operands.
Addressing modes involving
 registers have zero additional cost,
 while those involving a memory location or constant in them have an additional cost of
one, because such operands have to be stored in the words following the instruction.
Some examples:
1. The instruction LOD AX, BX copies the contents of register BX into register AX. This
instruction has a cost of one because no additional memory words are required.

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 and Flow Graphs


This section introduces a graph representation of intermediate code that is helpful for discussing
code generation even if the graph is not constructed explicitly by a code-generation algorithm.
Code generation benefits from context. We can do a better job of register allocation if we know
how values are defined and used, also we can do a better job of instruction selection by looking
at sequences of three-address statements
The representation is constructed as follows:
 Partition the intermediate code into basic blocks, which are maximal sequences of consecutive
three-address instructions with the properties that
1. The flow of control can only enter the basic block through the first instruction in the
block. That is, there are no jumps into the middle of the block.
2. Control will leave the block without halting or branching, except possibly at the last
instruction in the block.
 The basic blocks become the nodes of a flow graph, whose edges indicate which blocks can
follow which other blocks.

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

 Instruction 1 is a leader by rule (1) of Algorithm.


 To find the other leaders, we first need to find the jumps. In this example, there are three
jumps, all conditional, at instructions 9, 11, and 17. By rule (2), the targets of these jumps are
leaders; they are instructions 3, 2, and 13, respectively.
 Then, by rule (3), each instruction following a jump is a leader; those are instructions 10 & 12.
Note that no instruction follows 17 in this code, but if there were code following, the 18th
instruction would also be a leader.
We conclude that the leaders are instructions 1, 2, 3, 10, 12, and 13.
The basic block of each leader contains all the instructions from itself until just before the next
leader.
 Thus, the basic block of 1 is just 1,
 For leader 2 the block is just 2.
 Leader 3, however, has a basic block consisting of instructions 3 through 9, inclusive.
 For Leader 10 the block is 10 and 11;
 For leader 12 the block is just 12, and

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 Basic Blocks


DAG is used to do local optimization of s basic block
We construct a DAG for a basic blocks follows:
1. Leaves are labeled by identifiers, variable names, constants. Most leaves are r-values, they
represent initial values of names.
2. Interior nodes are labeled by an operator.
3. Interior initial value is name
4. The sequence of identifiers can also be the labels of nodes.

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

A Simple Code Generator


We shall consider an algorithm that generates code for a single basic block. It considers each
three-address instruction in turn, and keeps track of what values are in what registers so it can
avoid generating unnecessary loads and stores.
One of the primary issues during code generation is deciding how to use registers to best
advantage.

There are four principal uses of registers:

 In most machine architectures, some or all of the operands of an operation must be in


registers in order to perform the operation.
 Registers make good temporaries - places to hold the result of a sub-expression while a
larger expression is being evaluated, or more generally, a place to hold a variable that is
used only within a single basic block.
 Registers are used to hold (global) values that are computed in one basic block and used
in other blocks, for example, a loop index that is incremented going around the loop and
is used several times within the loop.
 Registers are often used to help with run-time storage management, for example, to
manage the run-time stack, including the maintenance of stack pointers and possibly the
top elements of the stack itself.
These are competing needs, since the number of registers available is limited. The
algorithm in this section assumes that some set of registers is available to hold the values that are
used within the block. Typically, this set of registers does not include all the registers of the
machine, since some registers are reserved for global variables and managing the stack.

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."

CRITERIA’S FOR CODE OPTIMIZATION

1. Transformation must preserve the meaning of program


An optimization must not change the output produced by a program for given input, or do not
cause an error, that are not present in original program.
2. Transformation must be take place for reducing time complexity of the program rather than
reducing space complexity.
That is we do transformation for speedup the program execution by measurable amount.
Sometime we are interested in reducing the space taken by program (compiled code).
3. Transformation must be worth effort.
It does not make sense to expend the time required for compilation for doing better
transformation Code optimization is one of the phase in the compilation process, if it took more time to
do better transformation for improvement then its of no use because it ultimately increase the compilation
time.
FUNCTION PRESERVING TRANSFORMATION
(SEMANTICS-PRESERVING TRANSFORMATIONS)
There are number of ways in which a compiler can improve a program without changing the function
it computes 1. Constant folding (compile time evaluation)
2. Elimination Common sub-expression,
3. Copy propagation
4. Dead-code elimination
These are common examples of such function-preserving (or semantics-preserving) transformations.
Constant folding (compile time evaluation)
There are some statements in the program which can be executed at compile time and hence it
reduce the execution time
Consider the statement x = 24 / 2 ; now we know 24 / 2 is 12 which is constant and we can
compute at compile time so we can replace x = 24 / 2 by x = 12 is called ad constant folding or compile
time evaluation.
Elimination of common sub expression
An occurrence of an expression E is called a common sub expression if E was previously
computed and the values of the variables in E have not changed since the previous computation. We
avoid re computing E if we can use its previously computed value; that is, the variable x to which the
previous computation of E was assigned has not changed in the interim.
Consider following snippet (We assume that for execution of each operator will take one unit time)

Before Time After Time Consider E = a + b is common sub


transformation complexity transformation complexity expression because it was previously
t= a+b; 2 computed and values of variables a and b
P=a+b+c; 3 P=t+c; 2 was not change from previous
Q=a+b+d; 3 Q= t+d; 2 computation, so we can store the result
R=a+b+e; 3 R= t+e; 2 the on a + b into temporary variable t and
S=a+b+f; 3 S = t+f; 2
replace each occurrence of a + b by t.
12 10

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 ;}

Strength reduction Before optimization After optimization


Strength reduction is replacing expensive operator by for ( j = 0 ; j > 10 ; j++ ) t=5;
cheaper one. { for ( j = 0 ; j > 10 ; j++ )
The precedence of multiplication is higher than x=j*5; { x=t;
addition, so can replace multiplication by successive } t=t+5;
addition }

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

Eliminating Unreachable Code


Another opportunity for peephole optimization is the removal of unreachable instructions. An
unlabeled instruction immediately following an unconditional jump may be removed. This operation can
be repeated to eliminate a sequence of instructions.
For example, In the above example instructions in the code are executed
……….. ; sequentionally and control reaches to goto statement which is
……….. ; unconditional jump, and goto statement shifted control to label L1
Goto L1 The statement Printf(“after unconditional jump”); Never get
Printf(“after unconditional jump”); executed this is unreachable code.
L1 : …………..
Another example In this code getch() in add() function is unreachable code because
int main (void) normal control flow will go in callee after return statement
{ int a=2,b=3,c;
c= add(a,b); printf(“%d”,c); }
int add (int i,int j)
{ return (i+j); getch();
}

Eliminating Redundant Loads and Stores


If we see the The assembly instruction In a target program, we can delete the store instruction because
code sequence whenever it is executed, the first instruction will ensure that
a=a; LOD a, AX the value of a has already been loaded into register AX
STO AX, a

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;

Use of Machine Idioms


The target machine may have hardware instructions to implement certain specific operations
efficiently. Detecting situations that permit the use of these instructions can reduce execution time
significantly.
For example, some machines have auto-increment and auto-decrement addressing modes. These
add or subtract one from an operand before or after using its value. The use of the modes greatly
improves the quality of code .These modes can also be used in code for statements like a = a + l .

Usual code Optimized code With special instruction


LOD a, AX LOD a, AX INCR a
LDI 1, BX ADI AX, 1 If machine given us INCR instruction which increments the
ADD AX, BX STO X contents of variable by 1
STO X

68

You might also like