0% found this document useful (0 votes)
40 views35 pages

Compler

compiler notes in summary

Uploaded by

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

Compler

compiler notes in summary

Uploaded by

Ajay Jha
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 35

Compiler Design | structure of a Compiler

Prerequisite – Introduction of Compiler design


We basically have two phases of compilers, namely Analysis phase and Synthesis phase. Analysis phase
creates an intermediate representation from the given source code. Synthesis phase creates an equivalent
target program from the intermediate representation.

Symbol Table – It is a data structure being used and maintained by the compiler, consists all the
identifier’s name along with their types. It helps the compiler to function smoothly by finding the
identifiers quickly.
The compiler has two modules namely front end and back end. Front-end constitutes of the Lexical
analyzer, semantic analyzer, syntax analyzer and intermediate code generator. And the rest are assembled
to form the back end.
1. Lexical Analyzer – It reads the program and converts it into tokens. It converts a stream of lexemes
into a stream of tokens. Tokens are defined by regular expressions which are understood by the
lexical analyzer. It also removes white-spaces and comments.
2. Syntax Analyzer – It is sometimes called as parser. It constructs the parse tree. It takes all the tokens
one by one and uses Context Free Grammar to construct the parse tree.
Why Grammar ?
The rules of programming can be entirely represented in some few productions. Using these
productions we can represent what the program actually is. The input has to be checked whether it is
in the desired format or not.
Syntax error can be detected at this level if the input is not in accordance with the grammar.

3. Semantic Analyzer – It verifies the parse tree, whether it’s meaningful or not. It furthermore
produces a verified parse tree.It also does type checking, Label checking and Flow control checking.
4. Intermediate Code Generator – It generates intermediate code, that is a form which can be readily
executed by machine We have many popular intermediate codes. Example – Three address code etc.
Intermediate code is converted to machine language using the last two phases which are platform
dependent.
Till intermediate code, it is same for every compiler out there, but after that, it depends on the
platform. To build a new compiler we don’t need to build it from scratch. We can take the
intermediate code from the already existing compiler and build the last two parts.
5. Code Optimizer – It transforms the code so that it consumes fewer resources and produces more
speed. The meaning of the code being transformed is not altered. Optimisation can be categorized into
two types: machine dependent and machine independent.
6. Target Code Generator – The main purpose of Target Code generator is to write a code that the
machine can understand and also register allocation, instruction selection etc. The output is dependent
on the type of assembler. This is the final stage of compilation.

Compiler Design - Lexical Analysis

Lexical analysis is the first phase of a compiler. It takes the modified source code from language
preprocessors that are written in the form of sentences. The lexical analyzer breaks these syntaxes into a
series of tokens, by removing any whitespace or comments in the source code.
If the lexical analyzer finds a token invalid, it generates an error. The lexical analyzer works closely with
the syntax analyzer. It reads character streams from the source code, checks for legal tokens, and passes
the data to the syntax analyzer when it demands.
Tokens
Lexemes are said to be a sequence of characters (alphanumeric) in a token. There are some predefined
rules for every lexeme to be identified as a valid token. These rules are defined by grammar rules, by
means of a pattern. A pattern explains what can be a token, and these patterns are defined by means of
regular expressions.
In programming language, keywords, constants, identifiers, strings, numbers, operators and punctuations
symbols can be considered as tokens.
For example, in C language, the variable declaration line
int value = 100;
contains the tokens:
int (keyword), value (identifier), = (operator), 100 (constant) and ; (symbol).

Specifications of Tokens
Let us understand how the language theory undertakes the following terms:

Alphabets
Any finite set of symbols {0,1} is a set of binary alphabets, {0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F} is a set of
Hexadecimal alphabets, {a-z, A-Z} is a set of English language alphabets.

Strings
Any finite sequence of alphabets is called a string. Length of the string is the total number of occurrence
of alphabets, e.g., the length of the string tutorialspoint is 14 and is denoted by |tutorialspoint| = 14. A
string having no alphabets, i.e. a string of zero length is known as an empty string and is denoted by ε
(epsilon).

Special Symbols
A typical high-level language contains the following symbols:-

Arithmetic Addition(+), Subtraction(-), Modulo(%),


Symbols Multiplication(*), Division(/)

Punctuation Comma(,), Semicolon(;), Dot(.), Arrow(->)


Assignment =

Special Assignment +=, /=, *=, -=

Comparison ==, !=, <, <=, >, >=

Preprocessor #

Location Specifier &

Logical &, &&, |, ||, !

Shift Operator >>, >>>, <<, <<<

Language
A language is considered as a finite set of strings over some finite set of alphabets. Computer languages
are considered as finite sets, and mathematically set operations can be performed on them. Finite
languages can be described by means of regular expressions.

Longest Match Rule


When the lexical analyzer read the source-code, it scans the code letter by letter; and when it encounters
a whitespace, operator symbol, or special symbols, it decides that a word is completed.
For example:
int intvalue;
While scanning both lexemes till ‘int’, the lexical analyzer cannot determine whether it is a
keyword int or the initials of identifier int value.
The Longest Match Rule states that the lexeme scanned should be determined based on the longest
match among all the tokens available.
The lexical analyzer also follows rule priority where a reserved word, e.g., a keyword, of a language is
given priority over user input. That is, if the lexical analyzer finds a lexeme that matches with any
existing reserved word, it should generate an error.
Compiler Design | Classification of top down parsers
Parsing is classified into two categories, i.e. Top Down Parsing and Bottom-Up Parsing. Top-Down
Parsing is based on Left Most Derivation whereas Bottom Up Parsing is dependent on Reverse Right
Most Derivation.
The process of constructing the parse tree which starts from the root and goes down to the leaf is Top-
Down Parsing.
 Top-Down Parsers constructs from the Grammar which is free from ambiguity and left recursion.
 Top Down Parsers uses leftmost derivation to construct a parse tree.
 It allows a grammar which is free from Left Factoring.
Classification of Top-Down Parsing –
1. With Backtracking: Brute Force Technique
2. Without Backtracking:1. Recursive Descent Parsing
2. Predictive Parsing or Non-Recursive Parsing or LL(1) Parsing or Table Driver Parsing
Brute Force Technique or Recursive Descent Parsing –
1. Whenever a Non-terminal spend first time then go with the first alternative and compare with the
given I/P String
2. If matching doesn’t occur then go with the second alternative and compare with the given I/P String.
3. If matching again not found then go with the alternative and so on.
4. Moreover, If matching occurs for at least one alternative, then the I/P string is parsed successfully.
LL(1) or Table Driver or Predictive Parser –
1. In LL1, first L stands for Left to Right and second L stands for Left-most Derivation. 1 stands for
number of Look Aheads token used by parser while parsing a sentence.
2. LL(1) parsing is constructed from the grammar which is free from left recursion, common prefix, and
ambiguity.
3. LL(1) parser depends on 1 look ahead symbol to predict the production to expand the parse tree.
4. This parser is Non-Recursive.
We have learnt in the last chapter that the top-down parsing technique parses the input, and starts
constructing a parse tree from the root node gradually moving down to the leaf nodes. The types of top-
down parsing are depicted below:

Recursive Descent Parsing


Recursive descent is a top-down parsing technique that constructs the parse tree from the top and the
input is read from left to right. It uses procedures for every terminal and non-terminal entity. This
parsing technique recursively parses the input to make a parse tree, which may or may not require back-
tracking. But the grammar associated with it (if not left factored) cannot avoid back-tracking. A form of
recursive-descent parsing that does not require any back-tracking is known as predictive parsing.
This parsing technique is regarded recursive as it uses context-free grammar which is recursive in nature.

Back-tracking
Top- down parsers start from the root node (start symbol) and match the input string against the
production rules to replace them (if matched). To understand this, take the following example of CFG:
S → rXd | rZd
X → oa | ea
Z → ai
For an input string: read, a top-down parser, will behave like this:
It will start with S from the production rules and will match its yield to the left-most letter of the input,
i.e. ‘r’. The very production of S (S → rXd) matches with it. So the top-down parser advances to the next
input letter (i.e. ‘e’). The parser tries to expand non-terminal ‘X’ and checks its production from the left
(X → oa). It does not match with the next input symbol. So the top-down parser backtracks to obtain the
next production rule of X, (X → ea).
Now the parser matches all the input letters in an ordered manner. The string is accepted.
Predictive Parser
Predictive parser is a recursive descent parser, which has the capability to predict which production is to
be used to replace the input string. The predictive parser does not suffer from backtracking.
To accomplish its tasks, the predictive parser uses a look-ahead pointer, which points to the next input
symbols. To make the parser back-tracking free, the predictive parser puts some constraints on the
grammar and accepts only a class of grammar known as LL(k) grammar.

Predictive parsing uses a stack and a parsing table to parse the input and generate a parse tree. Both the
stack and the input contains an end symbol $ to denote that the stack is empty and the input is consumed.
The parser refers to the parsing table to take any decision on the input and stack element combination.
In recursive descent parsing, the parser may have more than one production to choose from for a single
instance of input, whereas in predictive parser, each step has at most one production to choose. There
might be instances where there is no production matching the input string, making the parsing procedure
to fail.

LL Parser
An LL Parser accepts LL grammar. LL grammar is a subset of context-free grammar but with some
restrictions to get the simplified version, in order to achieve easy implementation. LL grammar can be
implemented by means of both algorithms namely, recursive-descent or table-driven.
LL parser is denoted as LL(k). The first L in LL(k) is parsing the input from left to right, the second L in
LL(k) stands for left-most derivation and k itself represents the number of look aheads. Generally k = 1,
so LL(k) may also be written as LL(1).

LL Parsing Algorithm
We may stick to deterministic LL(1) for parser explanation, as the size of table grows exponentially with
the value of k. Secondly, if a given grammar is not LL(1), then usually, it is not LL(k), for any given k.
Parsing | Set 2 (Bottom Up or Shift Reduce Parsers)
In this article, we are discussing the Bottom Up parser.
Bottom Up Parsers / Shift Reduce Parsers
Build the parse tree from leaves to root. Bottom-up parsing can be defined as an attempt to reduce the
input string w to the start symbol of grammar by tracing out the rightmost derivations of w in reverse.
Eg.

Classification of bottom up parsers

A general shift reduce parsing is LR parsing. The L stands for scanning the input from left to right and R
stands for constructing a rightmost derivation in reverse.
Benefits of LR parsing:
1. Many programming languages using some variations of an LR parser. It should be noted that C++ and
Perl are exceptions to it.
2. LR Parser can be implemented very efficiently.
3. Of all the Parsers that scan their symbols from left to right, LR Parsers detect syntactic errors, as soon
as possible.
Here we will look at the construction of GOTO graph of grammar by using all the four LR parsing
techniques. For solving questions in GATE we have to construct the GOTO directly for the given
grammar to save time.

LR(0) Parser
We need two functions –
Closure()
Goto()
Augmented Grammar
If G is a grammar with start symbol S then G’, the augmented grammar for G, is the grammar with new
start symbol S’ and a production S’ -> S. The purpose of this new starting production is to indicate to the
parser when it should stop parsing and announce acceptance of input.
Let a grammar be S -> AA
A -> aA | b
The augmented grammar for the above grammar will be
S’ -> S
S -> AA
A -> aA | b
LR(0) Items
An LR(0) is the item of a grammar G is a production of G with a dot at some position in the right side.
S -> ABC yields four items
S -> .ABC
S -> A.BC
S -> AB.C
S -> ABC.
The production A -> ε generates only one item A -> .ε
Closure Operation:
If I is a set of items for a grammar G, then closure(I) is the set of items constructed from I by the two
rules:
1. Initially every item in I is added to closure(I).
2. If A -> α.Bβ is in closure(I) and B -> γ is a production then add the item B -> .γ to I, If it is not
already there. We apply this rule until no more items can be added to closure(I).
Eg:

Goto Operation :
Goto(I, X) = 1. Add I by moving dot after X.
2. Apply closure to first step.
Construction of GOTO graph-
 State I0 – closure of augmented LR(0) item
 Using I0 find all collection of sets of LR(0) items with the help of DFA
 Convert DFA to LR(0) parsing table
Construction of LR(0) parsing table:
 The action function takes as arguments a state i and a terminal a (or $ , the input end marker). The
value of ACTION[i, a] can have one of four forms:
1. Shift j, where j is a state.
2. Reduce A -> β.
3. Accept
4. Error
 We extend the GOTO function, defined on sets of items, to states: if GOTO[I i , A] = Ij then GOTO
also maps a state i and a nonterminal A to state j.
Eg:
Consider the grammar S ->AA
A -> aA | b
Augmented grammar S’ -> S
S -> AA
A -> aA | b
The LR(0) parsing table for above GOTO graph will be –

Action part of the table contains all the terminals of the grammar whereas the goto part contains all the
nonterminals. For every state of goto graph we write all the goto operations in the table. If goto is applied
to a terminal than it is written in the action part if goto is applied on a nonterminal it is written in goto
part. If on applying goto a production is reduced ( i.e if the dot reaches at the end of production and no
further closure can be applied) then it is denoted as Ri and if the production is not reduced (shifted) it is
denoted as Si.
If a production is reduced it is written under the terminals given by follow of the left side of the
production which is reduced for ex: in I5 S->AA is reduced so R1 is written under the terminals in
follow(S)={$} (To know more about how to calculate follow function: Click here ) in LR(0) parser.
If in a state the start symbol of grammar is reduced it is written under $ symbol as accepted.
NOTE: If in any state both reduced and shifted productions are present or two reduced productions are
present it is called a conflict situation and the grammar is not LR grammar.

NOTE:
1. Two reduced productions in one state – RR conflict.
2. One reduced and one shifted production in one state – SR conflict.

If no SR or RR conflict present in the parsing table then the grammar is LR(0) grammar.
In above grammar no conflict so it is LR(0) grammar.

NOTE —In solving GATE question we don’t need to make the parsing table, by looking at the GOTO
graph only we can determine if the grammar is LR(0) grammar or not. We just have to look for conflicts
in the goto graph i.e if a state contains two reduced or one reduced and one shift entry for a TERMINAL
variable then there is a conflict and it is not LR(0) grammar. (In case of one shift with a VARIABLE and
one reduced there will be no conflict because then the shift entries will go to GOTO part of table and
reduced entries will go in ACTION part and thus no multiple entries).

Compiler Design | Syntax Directed Translation


Background : Parser uses a CFG(Context-free-Grammer) to validate the input string and produce output
for next phase of the compiler. Output could be either a parse tree or abstract syntax tree. Now to
interleave semantic analysis with syntax analysis phase of the compiler, we use Syntax Directed
Translation.
Definition
Syntax Directed Translation are augmented rules to the grammar that facilitate semantic analysis. SDT
involves passing information bottom-up and/or top-down the parse tree in form of attributes attached to
the nodes. Syntax directed translation rules use 1) lexical values of nodes, 2) constants & 3) attributes
associated to the non-terminals in their definitions.
The general approach to Syntax-Directed Translation is to construct a parse tree or syntax tree and
compute the values of attributes at the nodes of the tree by visiting them in some order. In many cases,
translation can be done during parsing without building an explicit tree.
Example
E -> E+T | T
T -> T*F | F
F -> INTLIT
This is a grammar to syntactically validate an expression having additions and multiplications in it. Now,
to carry out semantic analysis we will augment SDT rules to this grammar, in order to pass some
information up the parse tree and check for semantic errors, if any. In this example we will focus on
evaluation of the given expression, as we don’t have any semantic assertions to check in this very basic
example.
E -> E+T { E.val = E.val + T.val } PR#1
E -> T { E.val = T.val } PR#2
T -> T*F { T.val = T.val * F.val } PR#3
T -> F { T.val = F.val } PR#4
F -> INTLIT { F.val = INTLIT.lexval } PR#5
For understanding translation rules further, we take the first SDT augmented to [ E -> E+T ] production
rule. The translation rule in consideration has val as attribute for both the non-terminals – E & T. Right
hand side of the translation rule corresponds to attribute values of right side nodes of the production rule
and vice-versa. Generalizing, SDT are augmented rules to a CFG that associate 1) set of attributes to
every node of the grammar and 2) set of translation rules to every production rule using attributes,
constants and lexical values.
Let’s take a string to see how semantic analysis happens – S = 2+3*4. Parse tree corresponding to S
would be

To evaluate translation rules, we can employ one depth first search traversal on the parse tree. This is
possible only because SDT rules don’t impose any specific order on evaluation until children attributes
are computed before parents for a grammar having all synthesized attributes. Otherwise, we would have
to figure out the best suited plan to traverse through the parse tree and evaluate all the attributes in one or
more traversals. For better understanding, we will move bottom up in left to right fashion for computing
translation rules of our example.
Above diagram shows how semantic analysis could happen. The flow of information happens bottom-up
and all the children attributes are computed before parents, as discussed above. Right hand side nodes are
sometimes annotated with subscript 1 to distinguish between children and parent.
Additional Information
Synthesized Attributes are such attributes that depend only on the attribute values of children nodes.
Thus [ E -> E+T { E.val = E.val + T.val } ] has a synthesized attribute val corresponding to node E. If all
the semantic attributes in an augmented grammar are synthesized, one depth first search traversal in any
order is sufficient for semantic analysis phase.
Inherited Attributes are such attributes that depend on parent and/or siblings attributes.
Thus [ Ep -> E+T { Ep.val = E.val + T.val, T.val = Ep.val } ], where E & Ep are same production
symbols annotated to differentiate between parent and child, has an inherited attribute val corresponding
to node T.

TYPE CHECKING

A compiler must check that the source program follows both syntactic and semantic conventions
of the source language. This checking, called static checking, detects and reports programming errors.

Some examples of static checks:

1. Type checks - A compiler should report an error if an operator is applied to an incompatible operand.
Example: If an array variable and function variable are added together.

2. Flow-of-control checks - Statements that cause flow of control to leave a construct must have some
place to which to transfer the flow of control. Example: An enclosing statement, such as break, does not
exist in switch statement.
Fig. 2.6 Position of type checker
A typechecker verifies that the type of a construct matches that expected by its context. For
example : arithmetic operator mod in Pascal requires integer operands, so a type checker verifies that the
operands of mod have type integer. Type information gathered by a type checker may be needed when
code is generated.

Type Systems

The design of a type checker for a language is based on information about the syntactic constructs
in the language, the notion of types, and the rules for assigning types to language
constructs.
For example : “ if both operands of the arithmetic operators of +,- and * are of type integer, then the result
is of type integer ”

Type Expressions
The type of a language construct will be denoted by a “type expression.” A type expression is
either a basic type or is formed by applying an operator called a type constructor to other type
expressions. The sets of basic types and constructors depend on the language to be checked. The
following are the definitions of type expressions:
1. Basic types such as boolean, char, integer, real are type expressions.
A special basic type, type_error , will signal an error during type checking; void denoting “the
absence of a value” allows statements to be checked.

2. Since type expressions may be named, a type name is a type expression.


3. A type constructor applied to type expressions is a type expression.

Constructors include:

Arrays : If T is a type expression then array (I,T) is a type expression denoting the type of an
array with elements of type T and index set I.

Products : If T1 and T2 are type expressions, then their Cartesian product T1 X T2 is a type
expression.

Records : The difference between a record and a product is that the names. The record type
constructor will be applied to a tuple formed from field names and field types.

For example:

type row = record


address: integer;
lexeme: array[1..15] of char
end;
var table: array[1...101] of row;

declares the type name row representing the type expression record((address X integer) X
(lexeme X array(1..15,char))) and the variable table to be an array of records of this type.

Pointers : If T is a type expression, then pointer(T) is a type expression denoting the type “pointer
to an object of type T”.
For example, var p: ↑ row declares variable p to have type pointer(row).
Functions : A function in programming languages maps a domain type D to a range type R. The
type of such function is denoted by the type expression D → R
4. Type expressions may contain variables whose values are type expressions.

Fg. 5.7 Tree representation for char x char → pointer (integer)

Type systems

A type system is a collection of rules for assigning type expressions to the various parts of a
program. A type checker implements a type system. It is specified in a syntax-directed manner. Different
type systems may be used by different compilers or processors of the same language.

Static and Dynamic Checking of Types

Checking done by a compiler is said to be static, while checking done when the target program
runs is termed dynamic. Any check can be done dynamically, if the target code carries the type of an
element along with the value of that element.

Sound type system

A sound type system eliminates the need for dynamic checking fo allows us to determine
statically that these errors cannot occur when the target program runs. That is, if a sound type system
assigns a type other than type_error to a program part, then type errors cannot occur when the target code
for the program part is run.

Strongly typed language

A language is strongly typed if its compiler can guarantee that the programs it accepts will
execute without type errors.

Error Recovery
Since type checking has the potential for catching errors in program, it is desirable for type
checker to recover from errors, so it can check the rest of the input. Error handling has to be designed into
the type system right from the start; the type checking rules must be prepared to cope with errors.

SPECIFICATION OF A SIMPLE TYPE CHECKER

A type checker for a simple language checks the type of each identifier. The type checker is a
translation scheme that synthesizes the type of each expression from the types of its subexpressions. The
type checker can handle arrays, pointers, statements and functions.

A Simple Language
Consider the following grammar:
P→D;E
D → D ; D | id : T
T → char | integer | array [ num ] of T | ↑ T
E → literal | num | id | E mod E | E [ E ] | E ↑

Translation scheme:
P→D;E
D→D;D
D → id : T { addtype (id.entry , T.type) }
T → char { T.type : = char }
T → integer { T.type : = integer }
T → ↑ T1 { T.type : = pointer(T1.type) }
T → array [ num ] of T1 { T.type : = array ( 1… num.val , T1.type) }

In the above language,


→ There are two basic types : char and integer ; → type_error is used to signal errors;
→ the prefix operator ↑ builds a pointer type. Example , ↑ integer leads to the type expression

pointer ( integer ).

Type checking of expressions

In the following rules, the attribute type for E gives the type expression assigned to the expression
generated by E.

1. E → literal { E.type : = char } E→num { E.type : = integer }


Here, constants represented by the tokens literal and num have type char and integer.

2. E → id { E.type : = lookup ( id.entry ) }

lookup ( e ) is used to fetch the type saved in the symbol table entry pointed to by e.

3. E → E1 mod E2 { E.type : = if E1. type = integer and E2. type = integer then integer
else type_error }

The expression formed by applying the mod operator to two subexpressions of type integer has type
integer; otherwise, its type is type_error.
4. E → E1 [ E2 ] { E.type : = if E2.type = integer and E1.type = array(s,t) then t
else type_error }

In an array reference E1 [ E2 ] , the index expression E2 must have type integer. The result is the element
type t obtained from the type array(s,t) of E1.

5. E → E1 ↑ { E.type : = if E1.type = pointer (t) then t


else type_error }
The postfix operator ↑ yields the object pointed to by its operand. The type of E ↑ is the type t of
the object pointed to by the pointer E.
Type checking of statements

Statements do not have values; hence the basic type void can be assigned to them. If an error is
detected within a statement, then type_error is assigned.

Translation scheme for checking the type of statements:

1. Assignment statement: S→id: = E

2. Conditional statement: S→if E then S1

3. While statement:
S → while E do S1

4. Sequence of statements:

S → S1 ; S2 { S.type : = if S1.type = void and S1.type = void then void else type_error }

Type checking of functions


The rule for checking the type of a function application is : E → E1 ( E2) { E.type : = if E2.type = s and

E1.type = s → t then t else type_error }

Compiler Design | Runtime Environments


A translation needs to relate the static source text of a program to the dynamic actions that must occur at
runtime to implement the program. The program consists of names for procedures, identifiers etc., that
require mapping with the actual memory location at runtime.
Runtime environment is a state of the target machine, which may include software libraries, environment
variables, etc., to provide services to the processes running in the system.
SOURCE LANGUAGE ISSUES
Activation Tree
A program consist of procedures, a procedure definition is a declaration that, in its simplest form,
associates an identifier (procedure name) with a statement (body of the procedure). Each execution of
procedure is referred to as an activation of the procedure. Lifetime of an activation is the sequence of
steps present in the execution of the procedure. If ‘a’ and ‘b’ be two procedures then their activations will
be non-overlapping (when one is called after other) or nested (nested procedures). A procedure is
recursive if a new activation begins before an earlier activation of the same procedure has ended. An
activation tree shows the way control enters and leaves activations.
Properties of activation trees are :-
 Each node represents an activation of a procedure.
 The root shows the activation of the main function.
 The node for procedure ‘x’ is the parent of node for procedure ‘y’ if and only if the control flows
from procedure x to procedure y.
Example – Consider the following program of Quicksort
main() {

Int n;
readarray();
quicksort(1,n);
}

quicksort(int m, int n) {

Int i= partition(m,n);
quicksort(m,i-1);
quicksort(i+1,n);
}
The activation tree for this program will be:
First main function as root then main calls readarray and quicksort. Quicksort in turn calls partition and
quicksort again. The flow of control in a program corresponds to the depth first traversal of activation tree
which starts at the root.
CONTROL STACK AND ACTIVATION RECORDS
Control stack or runtime stack is used to keep track of the live procedure activations i.e the procedures
whose execution have not been completed. A procedure name is pushed on to the stack when it is called
(activation begins) and it is popped when it returns (activation ends). Information needed by a single
execution of a procedure is managed using an activation record or frame. When a procedure is called, an
activation record is pushed into the stack and as soon as the control returns to the caller function the
activation record is popped.

A general activation record consist of the following things:


 Local variables: hold the data that is local to the execution of the procedure.
 Temporary values: stores the values that arise in the evaluation of an expression.
 Machine status: holds the information about status of machine just before the function call.
 Access link (optional): refers to non-local data held in other activation records.
 Control link (optional): points to activation record of caller.
 Return value: used by the called procedure to return a value to calling procedure
 Actual parameters
Control stack for the above quicksort example:
SUBDIVISION OF RUNTIME MEMORY
Runtime storage can be subdivide to hold :
 Target code- the program code , it is static as its size can be determined at compile time
 Static data objects
 Dynamic data objects- heap
 Automatic data objects- stack

STORAGE ALLOCATION TECHNIQUES


I. Static Storage Allocation
 For any program if we create memory at compile time, memory will be created in the static area.
 For any program if we create memory at compile time only, memory is created only once.
 It don’t support dynamic data structure i.e memory is created at compile time and deallocated after
program completion.
 The drawback with static storage allocation is recursion is not supported.
 Another drawback is size of data should be known at compile time
Eg- FORTRAN was designed to permit static storage allocation.
II. Stack Storage Allocation
 Storage is organised as a stack and activation records are pushed and popped as activation begin and
end respectively. Locals are contained in activation records so they are bound to fresh storage in each
activation.
 Recursion is supported in stack allocation
III. Heap Storage Allocation
 Memory allocation and deallocation can be done at any time and at any place depending on the
requirement of the user.
 Heap allocation is used to dynamically allocate memory to the variables and claim it back when the
variables are no more required.
 Recursion is supported.

PARAMETER PASSING
The communication medium among procedures is known as parameter passing. The values of the
variables from a calling procedure are transferred to the called procedure by some mechanism.
Basic terminology :
 R- value: The value of an expression is called its r-value. The value contained in a single variable
also becomes an r-value if its appear on the right side of the assignment operator. R-value can always
be assigned to some other variable.
 L-value: The location of the memory(address) where the expression is stored is known as the l-value
of that expression. It always appears on the left side if the assignment operator.

i.Formal Parameter: Variables that take the information passed by the caller procedure are called
formal parameters. These variables are declared in the definition of the called function.
ii.Actual Parameter: Variables whose values and functions are passed to the called function are
called actual parameters. These variables are specified in the function call as arguments.
Different ways of passing the parameters to the procedure
 Call by Value
In call by value the calling procedure pass the r-value of the actual parameters and the compiler puts
that into called procedure’s activation record. Formal parameters hold the values passed by the calling
procedure, thus any changes made in the formal parameters does not affect the actual paramet

ers.
 Call by ReferenceIn call by reference the formal and actual parameters refers to same memory
location. The l-value of actual parameters is copied to the activation record of the called function.
Thus the called function has the address of the actual parameters. If the actual parameters does not
have a l-value (eg- i+3) then it is evaluated in a new temporary location and the address of the
location is passed. Any changes made in the formal parameter is reflected in the actual parameters
(because changes are made at the address).
 Call by Copy Restore
In call by copy restore compiler copies the value in formal parameters when the procedure is called
and copy them back in actual parameters when control returns to the called function. The r-values are
passed and on return r-value of formals are copied into l-value of actuals.

 Call by Name
In call by name the actual parameters are substituted for formals in all the places formals occur in the
procedure. It is also referred as lazy evaluation because evaluation is done on parameters only when

needed.

Compiler Design - Code Generation

Code generation can be considered as the final phase of compilation. Through post code generation,
optimization process can be applied on the code, but that can be seen as a part of code generation phase
itself. The code generated by the compiler is an object code of some lower-level programming language,
for example, assembly language. We have seen that the source code written in a higher-level language is
transformed into a lower-level language that results in a lower-level object code, which should have the
following minimum properties:

 It should carry the exact meaning of the source code.


 It should be efficient in terms of CPU usage and memory management.
We will now see how the intermediate code is transformed into target object code (assembly code, in this
case).

Directed Acyclic Graph


Directed Acyclic Graph (DAG) is a tool that depicts the structure of basic blocks, helps to see the flow
of values flowing among the basic blocks, and offers optimization too. DAG provides easy
transformation on basic blocks. DAG can be understood here:
 Leaf nodes represent identifiers, names or constants.
 Interior nodes represent operators.
 Interior nodes also represent the results of expressions or the identifiers/name where the values
are to be stored or assigned.
Example:
t0 = a + b
t1 = t0 + c
d = t0 + t1
[t0 = a + b]

[t1 = t0 + c]

[d = t0 + t1]

Peephole Optimization
This optimization technique works locally on the source code to transform it into an optimized code. By
locally, we mean a small portion of the code block at hand. These methods can be applied on
intermediate codes as well as on target codes. A bunch of statements is analyzed and are checked for the
following possible optimization:

Redundant instruction elimination


At source code level, the following can be done by the user:

int add_ten(int int add_ten(int int add_ten(int int add_ten(int


x) x) x) x)
{ { { {
int y, z; int y; int y = 10; return x + 10;
y = 10; y = 10; return x + y; }
z = x + y; y = x + y; }
return z; return y;
} }
At compilation level, the compiler searches for instructions redundant in nature. Multiple loading and
storing of instructions may carry the same meaning even if some of them are removed. For example:

 MOV x, R0
 MOV R0, R1
We can delete the first instruction and re-write the sentence as:
MOV x, R1

Unreachable code
Unreachable code is a part of the program code that is never accessed because of programming
constructs. Programmers may have accidently written a piece of code that can never be reached.
Example:
void add_ten(int x)
{
return x + 10;
printf(“value of x is %d”, x);
}
In this code segment, the printf statement will never be executed as the program control returns back
before it can execute, hence printf can be removed.

Flow of control optimization


There are instances in a code where the program control jumps back and forth without performing any
significant task. These jumps can be removed. Consider the following chunk of code:
...
MOV R1, R2
GOTO L1
...
L1 : GOTO L2
L2 : INC R1
In this code,label L1 can be removed as it passes the control to L2. So instead of jumping to L1 and then
to L2, the control can directly reach L2, as shown below:
...
MOV R1, R2
GOTO L2
...
L2 : INC R1

Algebraic expression simplification


There are occasions where algebraic expressions can be made simple. For example, the expression a = a
+ 0 can be replaced by a itself and the expression a = a + 1 can simply be replaced by INC a.

Strength reduction
There are operations that consume more time and space. Their ‘strength’ can be reduced by replacing
them with other operations that consume less time and space, but produce the same result.
For example, x * 2 can be replaced by x << 1, which involves only one left shift. Though the output of a
* a and a2 is same, a2 is much more efficient to implement.

Accessing machine instructions


The target machine can deploy more sophisticated instructions, which can have the capability to perform
specific operations much efficiently. If the target code can accommodate those instructions directly, that
will not only improve the quality of code, but also yield more efficient results.

Code Generator
A code generator is expected to have an understanding of the target machine’s runtime environment and
its instruction set. The code generator should take the following things into consideration to generate the
code:
 Target language : The code generator has to be aware of the nature of the target language for
which the code is to be transformed. That language may facilitate some machine-specific
instructions to help the compiler generate the code in a more convenient way. The target
machine can have either CISC or RISC processor architecture.
 IR Type : Intermediate representation has various forms. It can be in Abstract Syntax Tree
(AST) structure, Reverse Polish Notation, or 3-address code.
 Selection of instruction : The code generator takes Intermediate Representation as input and
converts (maps) it into target machine’s instruction set. One representation can have many ways
(instructions) to convert it, so it becomes the responsibility of the code generator to choose the
appropriate instructions wisely.
 Register allocation : A program has a number of values to be maintained during the execution.
The target machine’s architecture may not allow all of the values to be kept in the CPU memory
or registers. Code generator decides what values to keep in the registers. Also, it decides the
registers to be used to keep these values.
 Ordering of instructions : At last, the code generator decides the order in which the instruction
will be executed. It creates schedules for instructions to execute them.

Descriptors
The code generator has to track both the registers (for availability) and addresses (location of values)
while generating the code. For both of them, the following two descriptors are used:
 Register descriptor : Register descriptor is used to inform the code generator about the
availability of registers. Register descriptor keeps track of values stored in each register.
Whenever a new register is required during code generation, this descriptor is consulted for
register availability.
 Address descriptor : Values of the names (identifiers) used in the program might be stored at
different locations while in execution. Address descriptors are used to keep track of memory
locations where the values of identifiers are stored. These locations may include CPU registers,
heaps, stacks, memory or a combination of the mentioned locations.
Code generator keeps both the descriptor updated in real-time. For a load statement, LD R1, x, the code
generator:

 updates the Register Descriptor R1 that has value of x and


 updates the Address Descriptor (x) to show that one instance of x is in R1.

Code Generation
Basic blocks comprise of a sequence of three-address instructions. Code generator takes these sequence
of instructions as input.
Note : If the value of a name is found at more than one place (register, cache, or memory), the register’s
value will be preferred over the cache and main memory. Likewise cache’s value will be preferred over
the main memory. Main memory is barely given any preference.
getReg : Code generator uses getReg function to determine the status of available registers and the
location of name values. getReg works as follows:
 If variable Y is already in register R, it uses that register.
 Else if some register R is available, it uses that register.
 Else if both the above options are not possible, it chooses a register that requires minimal number
of load and store instructions.
For an instruction x = y OP z, the code generator may perform the following actions. Let us assume that
L is the location (preferably register) where the output of y OP z is to be saved:
 Call function getReg, to decide the location of L.
 Determine the present location (register or memory) of y by consulting the Address Descriptor
of y. If y is not presently in register L, then generate the following instruction to copy the value
of y to L:
MOV y’, L
where y’ represents the copied value of y.
 Determine the present location of z using the same method used in step 2 for y and generate the
following instruction:
OP z’, L
where z’ represents the copied value of z.
 Now L contains the value of y OP z, that is intended to be assigned to x. So, if L is a register,
update its descriptor to indicate that it contains the value of x. Update the descriptor of x to
indicate that it is stored at location L.
 If y and z has no further use, they can be given back to the system.
Other code constructs like loops and conditional statements are transformed into assembly language in
general assembly way.
Compiler Design - Code Optimization

Optimization is a program transformation technique, which tries to improve the code by making it
consume less resources (i.e. CPU, Memory) and deliver high speed.
In optimization, high-level general programming constructs are replaced by very efficient low-level
programming codes. A code optimizing process must follow the three rules given below:
 The output code must not, in any way, change the meaning of the program.
 Optimization should increase the speed of the program and if possible, the program should
demand less number of resources.
 Optimization should itself be fast and should not delay the overall compiling process.
Efforts for an optimized code can be made at various levels of compiling the process.
 At the beginning, users can change/rearrange the code or use better algorithms to write the code.
 After generating intermediate code, the compiler can modify the intermediate code by address
calculations and improving loops.
 While producing the target machine code, the compiler can make use of memory hierarchy and
CPU registers.
Optimization can be categorized broadly into two types : machine independent and machine dependent.

Machine-independent Optimization
In this optimization, the compiler takes in the intermediate code and transforms a part of the code that
does not involve any CPU registers and/or absolute memory locations. For example:
do
{
item = 10;
value = value + item;
} while(value<100);
This code involves repeated assignment of the identifier item, which if we put this way:
Item = 10;
do
{
value = value + item;
} while(value<100);
should not only save the CPU cycles, but can be used on any processor.

Machine-dependent Optimization
Machine-dependent optimization is done after the target code has been generated and when the code is
transformed according to the target machine architecture. It involves CPU registers and may have
absolute memory references rather than relative references. Machine-dependent optimizers put efforts to
take maximum advantage of memory hierarchy.

Basic Blocks
Source codes generally have a number of instructions, which are always executed in sequence and are
considered as the basic blocks of the code. These basic blocks do not have any jump statements among
them, i.e., when the first instruction is executed, all the instructions in the same basic block will be
executed in their sequence of appearance without losing the flow control of the program.
A program can have various constructs as basic blocks, like IF-THEN-ELSE, SWITCH-CASE
conditional statements and loops such as DO-WHILE, FOR, and REPEAT-UNTIL, etc.

Basic block identification


We may use the following algorithm to find the basic blocks in a program:
 Search header statements of all the basic blocks from where a basic block starts:
o First statement of a program.
o Statements that are target of any branch (conditional/unconditional).
o Statements that follow any branch statement.
 Header statements and the statements following them form a basic block.
 A basic block does not include any header statement of any other basic block.
Basic blocks are important concepts from both code generation and optimization point of view.
Basic blocks play an important role in identifying variables, which are being used more than once in a
single basic block. If any variable is being used more than once, the register memory allocated to that
variable need not be emptied unless the block finishes execution.

Control Flow Graph


Basic blocks in a program can be represented by means of control flow graphs. A control flow graph
depicts how the program control is being passed among the blocks. It is a useful tool that helps in
optimization by help locating any unwanted loops in the program.
Loop Optimization
Most programs run as a loop in the system. It becomes necessary to optimize the loops in order to save
CPU cycles and memory. Loops can be optimized by the following techniques:
 Invariant code : A fragment of code that resides in the loop and computes the same value at
each iteration is called a loop-invariant code. This code can be moved out of the loop by saving
it to be computed only once, rather than with each iteration.
 Induction analysis : A variable is called an induction variable if its value is altered within the
loop by a loop-invariant value.
 Strength reduction : There are expressions that consume more CPU cycles, time, and memory.
These expressions should be replaced with cheaper expressions without compromising the
output of expression. For example, multiplication (x * 2) is expensive in terms of CPU cycles
than (x << 1) and yields the same result.

Dead-code Elimination
Dead code is one or more than one code statements, which are:

 Either never executed or unreachable,


 Or if executed, their output is never used.
Thus, dead code plays no role in any program operation and therefore it can simply be eliminated.

Partially dead code


There are some code statements whose computed values are used only under certain circumstances, i.e.,
sometimes the values are used and sometimes they are not. Such codes are known as partially dead-code.
The above control flow graph depicts a chunk of program where variable ‘a’ is used to assign the output
of expression ‘x * y’. Let us assume that the value assigned to ‘a’ is never used inside the
loop.Immediately after the control leaves the loop, ‘a’ is assigned the value of variable ‘z’, which would
be used later in the program. We conclude here that the assignment code of ‘a’ is never used anywhere,
therefore it is eligible to be eliminated.

Likewise, the picture above depicts that the conditional statement is always false, implying that the code,
written in true case, will never be executed, hence it can be removed.

Partial Redundancy
Redundant expressions are computed more than once in parallel path, without any change in
operands.whereas partial-redundant expressions are computed more than once in a path, without any
change in operands. For example,
[redundant expression] [partially redundant
expression]

Loop-invariant code is partially redundant and can be eliminated by using a code-motion technique.
Another example of a partially redundant code can be:
If (condition)
{
a = y OP z;
}
else
{
...
}
c = y OP z;
We assume that the values of operands (y and z) are not changed from assignment of variable a to
variable c. Here, if the condition statement is true, then y OP z is computed twice, otherwise once. Code
motion can be used to eliminate this redundancy, as shown below:
If (condition)
{
...
tmp = y OP z;
a = tmp;
...
}
else
{
...
tmp = y OP z;
}
c = tmp;
Here, whether the condition is true or false; y OP z should be computed only once.
Recent development on compilers

 .Over the past decade, production compilers for general-purpose processors have adopted a
number of major technologies emerging from compiler research, including SSA-based
optimization, pointer analysis, profile-guided optimization, link-time cross-module
optimization, automatic vectorization, and just-in-time compilation with adaptive
optimization for dynamic languages. These features are here to stay for the foreseeable future.
So what major new features could emerge from compiler research over the next decade?
 First, just-in-time and dynamic optimization will be extended to static languages, such as C,
C++, and Fortran. This has already happened for graphics applications, as in the MacOS X
OpenGL library and the AMD ATI compiler, and is now being adopted for general-purpose
multicore platforms such as the RapidMind Multicore Development Platform.
 Second, and perhaps most predictably, compilers will play a major role in tackling the
multicore programming challenge. This does not mean that automatic parallelization will
come back from the dead. Rather compiler support for parallel programming will take two
forms: optimization and code generation for explicitly parallel programs; and interactive,
potentially optimistic, parallelization technology to support semi-automatic porting of
existing code to explicitly parallel programming models.
 Third, compilers will increasingly be responsible for enhancing or enforcing safety and
reliability properties for programs. The last few years have seen new language and compiler
techniques (e.g. in the Cyclone, CCured, and SAFECode projects) that guarantee complete
memory safety and sound operational semantics even for C and C++ programs. There is no
longer any excuse for production C/C++ compilers not to provide these capabilities, at least
as an option for security-sensitive software, including all privileged software.
 Furthermore, these capabilities can be deployed via a typed virtual machine that enables more
powerful security and reliability techniques than with native machine code.Fourth, compilers
will increasingly incorporate more sophisticated auto-tuning strategies for exploring
optimization sequences, or even arbitrary code sequences for key kernels. This is one of the
major sources of unexploited performance improvements with existing compiler technology.
 Finally, compilers will adopt speculative optimizations in order to compensate for the
constraints imposed by conservative static analysis. Recent architecture research has led to
novel hardware mechanisms that can make such speculation efficient and the ball is in the
compiler community's court to invent new ways to exploit this hardware support for more
powerful, traditional and non-traditional, optimizations.

You might also like