Compiler Design Internal Assessment Exam
Compiler Design Internal Assessment Exam
4. Consider the grammar E → E+E | E*E | id and parse the input id+id*id using Shift-Reduce A CO
parser 2
5. Give syntax directed definition for if-else statement. S CO
3
6. Convert the assignment statement d:= (a-b) + (a-c) + (a-c) into three address code. A CO
3
* *
b -c b -c
Operations on Languages
The following are the operations that can be applied to languages:
Union
Union of two languages Land M produces the set of strings which may be
either in language L or in language M or in both.
Example:
Let L and M be two languages where L = {dog, ba, na} and M = {house,
ba} then Union of L & U=LUM = {dog, ba, na, house}
Concatenation
Concatenation of two languages L and M, produces a set of strings which
are formed by merging the strings in L with strings in M (strings in L
must be followed by strings in M)
Example:
Concatenation of L & M = LM = {doghouse, dogba, bahouse, baba,
nahouse, naba}
Kleene closure
Kleene closure refers to zero or more occurrences of input symbols in a
string, i.e., it includes empty string Ɛ (set of strings with 0 or more
occurrences of input symbols). The kleene closure of language L, denoted
by L*.
For example, If L = {a, b}, then L* = { , a, b, aa, ab, ab, ba, bb, aaa, aba,
baa, . . . }
Positive closure
Positive closure indicates one or more occurrences of input symbols in a
string, i.e., it excludes empty string Ɛ (set of strings with 1 or more
occurrences of input symbols). The positive closure of Language L
denoted by L+. For example, If L = {a, b}, then
L+ = {a, b, aa, ba, bb, aaa, aba, . . . }
Precedence of operators
Unary operator (*) is having highest precedence.
Concatenation operator (.) is second highest and is left
associative.
Union operator (| or U) has least precedence and is left
associative.
(OR)
11. B 11. B. Construct the minimized DFA for the regular expression
(0+1)*(0+1)10.
12. A Construct a predictive parsing table for the following grammar and
also write the necessary algorithms. Show how the string(a, a) is (16)
parsed by the predictive parser S->a| ↑ | (T), T->T, S | S.
(OR)
12. B Consider the following grammar
S->L=R
S->R
L->*R
L->id
R->L
Discuss about LALR parsing method for the above grammar. List out
the canonical collection Items and also construct the parse table
13. A(i) Given the Syntax-Directed Definition below construct the annotated
parse tree for the input expression: “int a, b, c”.
Grammar Semantic Rule
D -> T L [Link] = [Link]
T -> int [Link] = integer
T -> float [Link] = float
L -> L1, id [Link] = [Link] addType([Link],[Link])
L -> idaddType([Link],[Link])
Nonterminal D represents a declaration, which, from production 1,
consists of a type T followed by a list L of identifiers. T has one attribute, T.
type, which is the type in the declaration D. Nonterminal L also has one
attribute, which we call inh to emphasize that it is an inherited attribute. The
purpose of [Link] is to pass the declared type down the list of identifiers, so
that it can be added to the appropriate symbol-table entries. Productions 2
and 3 each evaluate the synthesized attribute [Link], giving it the appropriate
value, integer or float. This type is passed to the attribute 1. the appropriate
for production 1. Production 4 passes [Link] down the pane tree. That is, the
value Linh is computed at a parse-tree node by copying the value of L. inh
from the parent of that node; the parent corresponds to the heal of the
production. Productions 4 and 5 also have a rule in which a function
addType is called with two arguments:
1. [Link], a lexical value that points to a symbol-table object, and
2. [Link], the type being assigned to every identifier on the list.
We suppose that function add Type properly installs the type [Link] as the
type of the represented identifier.
Indirect triples
In the indirect triple representation the listing of triples is been [Link]
listing pointers are used instead of using statements
14 A Explain in detail about the different storage allocation
From the perspective of the compiler writer, the executing target program
runs in its own logical address space in which each program value has a
location. The management and organization of this logical address space
is shared between the compiler, operating system, and target machine. The
operating system maps the logical addresses into physical addresses,
which are usually spread throughout [Link] storage comes into
blocks, where a byte is used to show the smallest unit of addressable
memory. Using the four bytes a machine word can form. Object of
multibyte is stored in consecutive bytes and gives the first byte address.
Run-time storage can be subdivide to hold the different components of an
executing program shown in Fig. 4.1:
Generated executable code
Static data objects
Dynamic data-object- heap
Automatic data objects- stack
STORAGE ALLOCATION STRATEGIES
The different storage allocation strategies are:
1. Static allocation - lays out storage for all data objects at compile time.
2. Stack allocation - manages the run-time storage as a stack.
[Link] allocation - allocates and deallocates storage as needed at run time
from a data area known as heap.
Static allocation
Static allocation is a procedure which is used for allocation of all the data
objects at compile time. Static allocation is possible only when the
compiler knows the size of data object at compile time. In this type of
allocation, formation of data objects is not possible under any
circumstances at run time. In static allocation, compiler decides the
amount of storage for each data object and binds the name of data objects
to the allocated storage. In static allocation, names are bound to storage
locations. If memory is created at compile time then the memory will be
created in static area and only once.
Advantages
It is easy to implement.
It allows type checking during compilation.
It eliminates the feasibility of running out of memory.
Disadvantages
It is incompatible with recursive subprograms.
It is not possible to use variables whose size has to be determined at run
time.
The static allocation can be completed if the size of the data object is
called compile time.
Stack allocation
Almost all compilers for languages that use procedures, functions, or
methods as units of user- defined actions manage at least part of their run-
time memory as a stack. Each time a procedure is called, space for its
local variables is pushed onto a stack, and when the procedure terminates,
that space is popped off the stack. This arrangement not only allows space
to be shared by procedure calls whose durations do not overlap in time,
but it allows us to compile code for a procedure in such a way that the
relative addresses of its nonlocal variables are always the same, regardless
of the
sequence of procedure calls. In stack storage allocation, storage is
organized as a stack. An activation record is pushed into the stack when
activation begins and it is popped when the activation end. Activation
record contains the locals so that they are bound to fresh storage in each
activation record. The value of locals is
deleted when the activation ends. It works on the basis of last-in-first-out
(LIFO) and this allocation
supports the recursion process.
Heap allocation
Heap allocation is the most flexible allocation scheme. Allocation and
deallocation of memory can be done at any time and at any place
depending upon the user's requirement. Heap allocation is used to allocate
memory to the variables dynamically and when the variables are no more
used then claim it back. Heap storage allocation supports the recursion
process.
or
14 B Discuss the various issues in code generation with necessary examples
The most important criterion for a code generator is that it produce correct
code. The following issues arises during the code generation phase.
Input to the Code Generator
The Target Program
Memory Management
Instruction Selection
Register Allocation
Evaluation Order
The front end has scanned, parsed, and translated the source program into
a relatively lowlevel IR, so that the values of the names appearing in the
IR can be represented by quantities that the target machine candirectly
manipulate, such as integers and floating-point numbers. All syntactic and
static semantic errors have been detected, that the necessary type checking
has taken place, and that typeconversion operators have been inserted
wherever necessary. The code generator can proceed on the assumption
that its input is error free.
The Target Program
The output of the code generator is the target program which is going to
run in the following
computers.
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 kept in registers.
The JVM is a software interpreter for Java byte codes, an
intermediate language produced by Java compilers. The interpreter
provides software compatibility across multiple platforms, a major
factor in the success of Java. To improve the high performance
interpretation just-intime (JIT) Java compilers have been created.
The output of the code generator may be:
Absolute machine language program: It can be placed in a fixed
memory location and immediately executed.
Reloadable machine-language program: It allows subprograms
(object modules) to be compiled separately. A set of relocatable
object modules can be linked together and loaded for execution by
a linking loader. the compiler must provide explicit relocation
information to the loader if automatic relocation is not possible.
Assembly language program: The process of code generation is
somewhat easier, but assembly must be converted into machine
executable code with help of assembler.
Memory Management
Names in the source program are mapped to addresses of data objects in
run-time memory by both the front end and code generator.
Memory Management uses symbol table to get names
information.
The amount of memory required by declared identifies are
calculated and storage space is reserved in memory at run time.
Labels in three address code are converted into equivalent
memory address.
For instance if a reference to ―goto j‖ is encountered in three
address code then appropriate jump instruction can be generated
by computing memory address for label j.
Some instruction address can be calculated in run time only that is
also after loading the program.
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 factors such as
The level of the intermediate representation (IR).
The nature of the instruction-set architecture.
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 uniformity and completeness of the instruction set are
important factors. The selection of instruction depends on the
instruction set of the target machine. Instruction speeds and
machine idioms are other important factors in selection of
instruction. If we do not care about the efficiency of the target
program, instruction 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, every three-address statement of the form x = y + z,
where x, y, and z are
statically allocated, can be translated into the code sequence.
LD R0, y // R0 = y (load y into register RO)
ADD R0, R0, z // R0 = R 0 + z (add z to R0)
ST x, R0 // x = R0 (store RO into x)
This strategy often produces redundant loads and stores. For
example, the sequence of threeaddress
statements
a=b+c
d=a+e
would be translated into the following code
LD R0, b // R0 = b
ADD R0, R0, c // R0 = R0 + c
ST a, R0 // a = R0 LD R0, a // R0 = a
ADD R0, R0, e // R0 = R0 + e
ST d, R0 // d = R0
Here, the fourth statement is redundant since it loads a value that
has just been stored, and so is the third if a
is not subsequently used.
The quality of the generated code is usually determined by its
speed and size. On most machines, agiven IR program can be
implemented by many different code sequences, with significant
cost differences between the different implementations. A naive
translation of the intermediate code may therefore lead to correct
but unacceptably inefficient target code.
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 subproblems:
Register allocation, during which we select the set of variables
that will reside in registers at eachpoint in the program.
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. Certain machines require
register-pairs for some operands and results Consider the two
three-address code sequences, the only difference is the operator
in the second statement.
t=a+b
t=t*c
t=t/d
The efficient Optimal machine-code sequences with only one
register
R0 LD R0, a
ADD R0, b
MUL R0, c
DIV R0, d
ST R0, t
Evaluation Order
The evaluation order is an important factor in generating an
efficient target code. 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. We can 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.
Loops:
Loops are important because programs spend most of their time executing
them, and optimizations that improve the performance of loops can have a
significant impact. Thus, it isessential that we identify loops and treat
them specially.
Loops also affect the running time of program analyses. If a program does
not contain anyloops, we can obtain the answers to data-flow problems by
making just one pass through the program. For example, a forward data-
flow problem can be solved by visiting all the nodes once, in topological
order.
Dominators :
In a flow graph, a node d dominates node n, if every path from initial node
of the flow graph to n goes through d. This will be denoted by d dom n.
Every initial node dominates all the remaining nodes in the flow graph
and the entry of a loop dominates all nodes in the loop. Similarly every
node dominates itself.
Inner loops:
If we use the natural loops as “the loops”, then we have the useful
property that unless two loops have the same header, they are either
disjointed or one is entirely contained in the other. Thus,neglecting loops
with the same header for the moment, we have a natural notion of inner
loop: one that contains no other loop. When two natural loops have the
same header, but neither is nested within the other, they are combined and
treated as a single loop.
Pre-Headers:
Several transformations require us to move statements “before the
header”. Therefore begin treatment of a loop L by creating a new block,
called the preheader. The pre-header has only the header as successor, and
all edges which formerly entered the header of L from outside L instead
enter the pre-header. Edges from inside loop L to the header are not
changed. Initially the pre- header is empty, but transformations on L may
place statements in it.
Or
15 B How is the optimization of basic block performed in compliers?
Explain it with necessary examples.
Optimization is applied to the basic blocks after the intermediate code
generation phase of the compiler. Optimization is the process of
transforming a program that improves the code by consuming fewer
resources and delivering high speed. In optimization, high-level codes are
replaced by their equivalent efficient low-level codes. Optimization of
basic blocks can be machine-dependent or machine-independent. These
transformations are useful for improving thequality of code that will be
ultimately generated from basic block.
There are two types of basic block optimizations:
1. Structure preserving transformations
2. Algebraic transformations
5.4.1 Structure preserving transformations
The structure-preserving transformation on basic blocks includes:
1. Dead Code Elimination
2. Common Subexpression Elimination
3. Renaming of Temporary variables
4. Interchange of two independent adjacent statements
1. Common sub-expression elimination:
In the common sub-expression, we don't need to be computed it over and
over again.
Instead of this we can compute it once and kept in store from where it's
referenced when
encountered again.
1. a : = b + c
2. b : = a - d
3. c : = b + c
4. d : = a - d
In the above expression, the second and forth expression computed the
same expression.
So the block can be transformed as follows:
1. a : = b + c
2. b : = a - d
3. c : = b + c
4. d : = b
2. Dead-code elimination
o It is possible that a program contains a large amount of dead code.
o This can be caused when once declared and defined once and forget to
remove them in this case they serve no purpose.
o Suppose the statement x:= y + z appears in a block and x is dead symbol
that means it will never subsequently used. Then without changing the
value of the basic block can safely remove this statement.
3. Renaming temporary variables
A statement t:= b + c can be changed to u:= b + c where t is a temporary
variable and u is a new temporary variable. All the instance of t can be
replaced with the u without changing the basic block value.
4. Interchange of statement
Suppose a block has the following two adjacent statements:
1. t1 : = b + c
2. t2 : = x + y
These two statements can be interchanged without affecting the value of
block when value of t1 does not affect the value of t2.
Algebraic transformations
Countless algebraic transformations can be used to change the set of
expressions computed by a basic block into an algebraically equivalent
set. Some of the algebraic transformation on basic blocks includes:
1. Constant Folding
2. Copy Propagation
3. Strength Reduction
1. Constant Folding:
Solve the constant terms which are continuous so that compiler does not
need to solve this expression.
Example:
x = 2 * 3 + y ⇒ x = 6 + y (Optimized code)
2. Copy Propagation:
It is of two types, Variable Propagation, and Constant Propagation.
Variable Propagation:
Constant Propagation:
3. StrengthReductin:
x = y ⇒ z = y + 2 (Optimized code)
z=x+2
x = 3 ⇒ z = 3 + a (Optimized code)
z=x+a
Replace expensive statement/ instruction with cheaper ones.
x = 2 * y (costly) ⇒ x = y + y (cheaper)
x = 2 * y (costly) ⇒ x = y << 1 (cheaper)
Staff In-charge HoD