Compiler DesignAnswer Key
Compiler DesignAnswer Key
PART - B (5 X 13 = 65 Marks)
11. a) Explain the various phases of compiler and trace i with the program segment
i=i*70+j+2.
A compiler is a computer program that decodes computer code composed in one
programming language into another language.
The 6 phases of a compiler are:
i. Lexical Analysis
ii. Syntactic Analysis or Parsing
iii. Semantic Analysis
iv. Intermediate Code Generation
v. Code Optimization
vi. Code Generation
1. Lexical Analysis:
Lexical analysis or Lexical analyzer is the initial stage or phase of the compiler. This phase
scans the source code and transforms the input program into a series of a token.
A token is basically the arrangement of characters that defines a unit of information in the source
code.
2. Syntax Analysis:
In the compilation procedure, the Syntax analysis is the second stage. Here the provided
input string is scanned for the validation of the structure of the standard grammar. Basically, in the
second phase, it analyses the syntactical structure and inspects if the given input is correct or not in
terms of programming syntax
3. Semantic Analysis:
In the process of compilation, semantic analysis is the third phase. It scans whether the
parse tree follows the guidelines of language. It also helps in keeping track of identifiers and
expressions. In simple words, we can say that a semantic analyzer defines the validity of the parse
tree, and the annotated syntax tree comes as an output.
4. Intermediate Code Generation:
The parse tree is semantically confirmed; now, an intermediate code generator develops
three address codes. A middle-level language code generated by a compiler at the time of the
translation of a source program into the object code is known as intermediate code or text.
5. Code optimizer:
Now coming to a phase that is totally optional, and it is code optimization. It is used to
enhance the intermediate code. This way, the output of the program is able to run fast and consume
less space. To improve the speed of the program, it eliminates the unnecessary strings of the code
and organizes the sequence of statements.
6. Code Generator:
The final stage of the compilation process is the code generation process. In this final
phase, it tries to acquire the intermediate code as input which is fully optimized and map it to the
machine code or language. Later, the code generator helps in translating the intermediate code into
the machine code.
11. b) (i) Discuss in detail about the lexical analysis phase with the possible error recovery
schemes.
Lexical Analysis:
Lexical analysis is the first phase of the compiler where the source code is scanned and
broken down into meaningful components called tokens. These tokens are the smallest units of a
program, such as keywords, identifiers, constants, operators, and punctuation symbols. The
primary goal of lexical analysis is to transform the source code into a sequence of tokens for further
processing by the compiler.
Overview of how lexical analysis works:
i. Scanning: The source code is read character by character, and sequences of characters are
matched against predefined patterns to identify tokens.
ii. Tokenization: Once a sequence of characters matches a pattern, it is recognized as a token
and classified accordingly (e.g., as a keyword, identifier, operator, etc.).
iii. Error Handling: If the scanner encounters invalid characters or sequences that don't
match any predefined patterns, it generates a lexical error.
iv. Output: Finally, the scanner produces a stream of tokens, which are passed on to the next
phase of the compiler for further processing.
Error Recovery Schemes:
Error recovery in lexical analysis refers to the strategies used to handle lexical errors
encountered during scanning. Here are some common error recovery schemes:
i. Panic Mode Recovery: In this scheme, the scanner skips ahead in the source code until it
finds a predefined "synchronization token" before resuming normal scanning. For
example, if an error is encountered, the scanner might skip ahead until it finds the next
semicolon or closing brace.
ii. Global Correction: Instead of just skipping ahead, the scanner might attempt to identify
the likely point of error recovery based on the structure of the language and continue
scanning from there. This approach often involves more sophisticated algorithms to
determine the most appropriate recovery point.
iii. Error Reporting: Along with error recovery, it's essential to provide informative error
messages to the user, indicating the location and nature of the lexical error. This helps the
programmer quickly identify and fix the issue in the source code.
iv. Partial Token Recovery: Sometimes, the scanner can recover from errors within a token
and continue scanning the rest of the source code. For example, if a numeric constant is
missing its closing quote, the scanner might still recognize the numeric part of the token
and continue scanning.
v. Interactive Error Handling: In interactive environments like IDEs, the lexical analyzer
might provide real-time feedback to the user, highlighting lexical errors as they type and
offering suggestions for correction.
Each error recovery scheme has its advantages and disadvantages, and the choice of scheme
often depends on factors such as the complexity of the language, the desired level of error
tolerance, and the performance requirements of the compiler.
11. b) (ii) Write the regular Expression for Delimiters and keywords and draw the
transition
diagram for the same.
(6)
Regular Expressions for Delimiters and Keywords
Delimiters
Common delimiters in programming languages include symbols like commas,
semicolons, parentheses, braces, and brackets. Here are the regular expressions for these
delimiters:
Comma: ,
Semicolon: ;
Parentheses: \( and \)
Braces: { and }
Brackets: \[ \]
The combined regular expression for all delimiters can be written as:
[,;(){}[\]]
Keywords
Keywords are reserved words in a programming language that have a predefined
meaning. Here is a list of some common keywords and their regular expressions:
if: if
else: else
while: while
return: return
int: int
float: float
void: void
The combined regular expression for these keywords can be written as:
if|else|while|return|int|float|void
Transition Diagram
A transition diagram for delimiters and keywords involves creating states and
transitions based on the input characters. Here’s a simplified version of such a diagram.
Delimiters Transition Diagram
State 0:
, --> State 1
; --> State 2
( --> State 3
) --> State 4
{ --> State 5
} --> State 6
[ --> State 7
] --> State 8
Each state corresponds to recognizing one specific delimiter.
Keywords Transition Diagram
Here’s a transition diagram for a few keywords (if, else, while, return, int,
float, void):
State 0:
i --> State 1
e --> State 9
w --> State 15
r --> State 22
f --> State 29
v --> State 35
State 1:
f --> State 2 (if recognized)
n --> State 3
t --> State 4
State 3:
t --> State 4
State 4:
(end of keyword int, return recognized)
State 9:
l --> State 10
State 10:
s --> State 11
State 11:
e --> State 12 (else recognized)
State 15:
h --> State 16
State 16:
i --> State 17
State 17:
l --> State 18
State 18:
e --> State 19 (while recognized)
State 22:
e --> State 23
State 23:
t --> State 24
State 24:
u --> State 25
State 25:
r --> State 26
State 26:
n --> State 27 (return recognized)
State 27: (return recognized)
State 29:
l --> State 30
State 30:
o --> State 31
State 31:
a --> State 32
State 32:
t --> State 33 (float recognized)
State 35:
o --> State 36
State 36:
i --> State 37
State 37:
d --> State 38 (void recognized)
12. a) (ii) Explain the operations on strings with suitable example for each.
(6)
The string operations are shown in the following table.
Traditional
Operation Free-Form Syntax
Syntax
CAT (Concatenate
Concatenate + operator
Two Strings)
%CONCAT (Concatenate with
Concatenate strings with a separator
Separator)
Concatenate array elements with a %CONCATARR (Concatenate
separator Array Elements with Separator)
%LOWER (Convert to Lower
Convert to lower case
Case)
%UPPER (Convert to Upper
Convert to upper case
Case)
CHECK (Check
Check %CHECK (Check Characters)
Characters)
CHECKR (Check
Check Reverse %CHECKR (Check Reverse)
Reverse)
%STR (Get or Store
Create
Null-Terminated String)
%REPLACE (Replace Character
Replace
String)
SCAN (Scan
Scan %SCAN (Scan for Characters)
String)
%SCANR (Scan Reverse for
Scan Reverse
Characters)
%SCANRPL (Scan and Replace
Scan and Replace
Characters)
%SPLIT (Split String into
Split a string
Substrings)
Traditional
Operation Free-Form Syntax
Syntax
%SUBST (Get Substring)
%LEFT (Get Leftmost
Substring SUBST (Substring) Characters)
%RIGHT (Get Rightmost
Characters)
Translate XLATE (Translate) %XLATE (Translate)
%TRIM (Trim Characters at
Edges)
%TRIML (Trim Leading
Trim Blanks
Characters)
%TRIMR (Trim Trailing
Characters)
Return the leftmost characters of a %LEFT (Get Leftmost
string Characters)
Return the number of bytes for
alphanumeric or double bytes for %LEN (Get or Set Length)
UCS-2 and graphic
Return the number of natural %CHARCOUNT (Return the
characters Number of Characters)
Return the rightmost characters of a %RIGHT (Get Rightmost
string Characters)
%STR (Get or Store
Work with a null-terminated string
Null-Terminated String)
Table 1. String Operations
The string operations include concatenation, scanning, substringing, translation,
and verification. String operations can only be used on character, graphic, or
UCS-2 fields.
The CAT operation concatenates two strings to form one.
The CHECK and CHECKR operations verify that each character in factor 2 is
among the valid characters in factor 1. CHECK verifies from left to right and
CHECKR from right to left.
The SCAN operation scans the base string in factor 2 for occurrences of another
string specified in factor 1.
The SUBST operation extracts a specified string from a base string in factor 2. The
extracted string is placed in the result field.
The XLATE operation translates characters in factor 2 according to the from and to
strings in factor 1.
(OR)
b) Explain the lex program for tokens and describe in detail the tool for generating
lexical analyzer with an example program (13)
Lex is a powerful tool used for generating lexical analyzers, also known as scanners
or tokenizers, for processing text. It operates by recognizing patterns in an input stream and
translating those patterns into a series of tokens for further processing by a parser or other
components of a compiler or interpreter.
Here's a breakdown of how Lex works:
1. Defining Patterns: In Lex, you start by defining patterns using regular
expressions. Regular expressions are a concise way of describing sets of strings.
For example, the regular expression "[a-zA-Z]+" matches one or more
occurrences of any uppercase or lowercase letter.
2. Associating Actions: Along with each pattern, you can specify an action to be
performed when that pattern is matched. Actions are typically written in C, though
Lex supports other programming languages as well. These actions can include
tasks like outputting a token, manipulating the input buffer, or invoking other
functions.
3. Compilation: Once you've defined your patterns and associated actions in a Lex
file (usually with a .l extension), you compile it using the Lex compiler (lex or
flex). This produces a C program that implements the lexical analyzer.
4. Integration with Other Components: Finally, you integrate the generated lexical
analyzer into your larger program. This typically involves incorporating the
generated C code into your project and linking it with other components, such as a
parser.
13. a) Compute if the following grammar is a LALR grammar or not and parse the
string.
Cdd
S->CC
C->cC|d. (13)
Solution:
Step1 − Construct LR (1) Set of items. First of all, all the LR (1) set of items should be
generated.
In these states, states I3 and I6 can be merged because they have the same core or
first component but a different second component of Look Ahead.
Similarly, states I4 and I7 are the same.
Similarly, states I8 and I9 are the same.
So, I3 and I6 can be combined to make I36.
I4 and I7 combined to make I47.
I8 and I9 combined to make I89.
So, the states will be
∴ I3 = goto (I0, c)
But I3 , I6 combined to make I36
∴ I36 = goto (I0, c)
∴ I4 = goto (I0, d)
But I4 , I7 combined to make I47
∴ I47 = goto (I0, d)
∴ I6 = goto (I2, c)
∴ I36 = goto (I2, c)
∴ I7 = goto (I2, d)
∴ I47 = goto (I2, d)
∴ goto (I3, C) = I8
But I8 is now part of I89
∴ goto (I36, C) = I89
Similarly,goto (I3, d) = I4, goto (I6, d) = I7 ∴ goto (I36, d) = I47
Construction of LALR Parsing Table
Filling of "𝐬𝐡𝐢𝐟𝐭" Entries(s)
Consider goto(I0, c) = I36
∴ Action[0, c] = s36
∴ Write s36 in front of Row state 0 and column c.
Similarly, consider
goto(I2, d) = I47
∴ Action[2, d] = 47
∴ Write s47 in front of Row State 2 and column d.
Filling the "𝐫𝐞𝐝𝐮𝐜𝐞" Entries (r)
Consider productions of the form A → α ∙ ,
For example, Consider State
I47 = goto(I0, d)
C → d ∙, c |d |$
∴ C → d ∙, c |d |$ is of form A → α ∙ , a.
Since C → d is production number (3) in given Question.
∴ Write r3 in front of Row State 47 and column c, d, $.
Because c, d looks ahead symbols in production C → d ∙ , c | d.
Filling of goto Entries
It can found out only for Non-Terminal.
For example, Consider
goto(I0, S) = I1
∴ goto [0, S] = 1
(OR)
b) Construct a predictive parsing table for the following grammar and parse the string.
Id+id*id
E-E+T|T
T-T*F|F
F->(E)|id.
(13)
Given grammar:
E -> E + T | T
T -> T * F | F
F -> ( E ) | id
Step 1: Rewrite the Grammar to Eliminate Left Recursion
To create a predictive parsing table, we need to eliminate left recursion in the
grammar.
1. Eliminate Left Recursion for `E`:
E -> TE'
E' -> +TE' | ε
2. Eliminate Left Recursion for `T:
T -> FT'
F` remains the same:
F -> ( E ) | id
Rewritten grammar:
E -> TE'
E' -> +TE' | ε
T -> FT'
T' -> *FT' | ε
F -> ( E ) | id
Step 2: Compute First and Follow Sets
1. First Sets:
First(E) = First(T) = First(F) = {id, (}
First(E') = {+, ε}
First(T') = {*, ε}
2. Follow Sets:
Follow(E) = Follow(E') = {$, )}
Follow(T)* = Follow(T') = {+, $, )}
Follow(F) = {*, +, $, )}
Step 3: Construct the Predictive Parsing Table
Non-Terminal id + * ( ) $
E E->TE’ E->TE’
E’->+TE
E
E’-> ᵋ E’-> ᵋ
T T->FT’ T->FT’
T’->*FT
T
T’-> ᵋ T’-> ᵋ T’-> ᵋ
F F->id F->(E)
14. a) Demonstrate the following : (i) Syntax Tree. (ii) Quadruples. (iii) Triples. (iv)
Indirect Triples using the arithmetic expression.
A=b*-c+b*-c, by converting it into three address code (13)
First need to convert the expression into three-address code. Here are the steps:
Converting to Three-Address Code
First, break down the expression ( A = b * -c + b * -c ) into smaller, manageable parts.
1. `t1 = -c`
2. `t2 = b * t1`
3. `t3 = -c`
4. `t4 = b * t3`
5. `t5 = t2 + t4`
6. `A = t5`
(i) Syntax Tree
The syntax tree represents the hierarchical structure of the expression. Here is the syntax
tree for ( A = b * -c + b * -c ):
=
/\
A +
/\
* *
/\ /\
b -c b -c
(ii) Quadruples
Quadruples represent each operation in four fields: operator, argument1, argument2, and
result.
| Operator | Arg1 | Arg2 | Result
|-------------|--------|-------|--------|
|- |c | | t1
|* |b | t1 | t2
|- |c | | t3
|* |b | t3 | t4
|+ | t2 | t4 | t5
|= | t5 | |A
(iii) Triples
Triples use the index of the temporary results instead of variable names.
| Index | Operator | Arg1 | Arg2 |
|---------|------------|--------|--------|
|0 |- |c |
|1 |* |b | (0)
|2 |- |c |
|3 |* |b | (2)
|4 |+ | (1) | (3)
|5 |= | (4) | A
(iv) Indirect Triples
Indirect triples use pointers (or array indices) to reference the triples.
| Index | Triple Pointer |
|---------|------------------|
|0 | (0) |
|1 | (1) |
|2 | (2) |
|3 | (3) |
|4 | (4) |
|5 | (5) |
Triples table (referenced by the indices above):
(OR)
14. b) Illustrate in detail about the translation for flow of control statements and find the
three- address code for the following
While a<b does
If c<d then
x=y+z
else
x=y-z. (13)
Translation for Flow of Control Statements
Flow of control statements include constructs like loops (`while`, `for`) and conditionals
(`if`, `else`). The translation of these statements into three-address code involves creating labels for
different parts of the code and using conditional and unconditional jumps.
While Loop
A `while` loop is translated by placing the test condition at the beginning and using jumps
to repeat the loop or exit based on the condition.
If-Else Statement
An `if-else` statement is translated by evaluating the condition and then using jumps to
execute the appropriate block of code based on the result of the condition.
Given Pseudocode
while a < b do
if c < d then
x=y+z
else
x=y–z
Three-Address Code Generation
1. Start of the `while` loop:
Test condition `a < b`.
Jump to the end if the condition is false.
2. Inside the `while` loop:
Test the `if` condition `c < d`.
Jump to the `else` block if the condition is false.
Execute the `then` block if the condition is true.
Jump to the end of the `if-else` block.
3. End of the `if-else` block:
Jump back to the beginning of the `while` loop to re-evaluate the condition.
4. End of the `while` loop:
Mark the end of the loop execution.
Three-Address Code
1. `L1: if a >= b goto L2`
2. `L3: if c >= d goto L4`
3. `x = y + z`
4. `goto L5`
5. `L4: x = y - z`
6. `L5: goto L1`
7. `L2:`
Explanation of the Three-Address Code
1. L1: Label for the start of the `while` loop.
2. if a >= b goto L2: If `a` is not less than `b`, exit the loop by jumping to `L2`.
3. L3: Label for the `if` condition inside the loop.
4. if c >= d goto L4: If `c` is not less than `d`, jump to the `else` block at `L4`.
5. x = y + z: If `c` is less than `d`, execute `x = y + z`.
6. goto L5: After executing the `then` block, jump to `L5` to skip the `else` block.
7. L4: Label for the `else` block.
8. x = y - z: If `c` is not less than `d`, execute `x = y - z`.
9. L5: Label for the end of the `if-else` block.
10. goto L1: After completing the `if-else` block, re-evaluate the `while` loop condition.
11. L2: Label for the end of the `while` loop.
1. Static Allocation
Static allocation lays out or assigns the storage for all the data objects at the
compile time. In static allocation names are bound to storage. The address of these
identifiers will be the same throughout. The memory will be allocated in a static location
once it is created at compile time. C and C++ use static allocation.
For example:
int number = 1;
static int digit = 1;
i. It is easy to understand.
ii. The memory is allocated once only at compile time and remains the same
throughout the program completion.
iii. Memory allocation is done before the program starts taking memory only on
compile time.
2. Heap Allocation
Heap allocation is used where the Stack allocation lacks if we want to retain the
values of the local variable after the activation record ends, which we cannot do in stack
allocation, here LIFO scheme does not work for the allocation and de-allocation of the
activation record. Heap is the most flexible storage allocation strategy we can
dynamically allocate and de-allocate local variables whenever the user wants according
to the user needs at run-time. The variables in heap allocation can be changed according
to the user’s requirement. C, C++, Python, and Java all of these support Heap Allocation.
For example:
i. Heap allocation is useful when we have data whose size is not fixed and can
change during the run time.
ii. We can retain the values of variables even if the activation records end.
iii. Heap allocation is the most flexible allocation scheme.
3. Stack Allocation
For example:
The input to the code generator is the intermediate code generated by the front end,
along with information in the symbol table that determines the run-time addresses of the
data objects denoted by the names in the intermediate representation. Intermediate codes
may be represented mostly in quadruples, triples, indirect triples, Postfix notation, syntax
trees, DAGs, etc. The code generation phase just proceeds on an assumption that the
input is free from all syntactic and state semantic errors, the necessary type checking has
taken place and the type-conversion operators have been inserted wherever necessary.
Target program
The target program is the output of the code generator. The output may be
absolute machine language, relocatable machine language, or assembly language.
Absolute machine language as output has the advantages that it can be
placed in a fixed memory location and can be immediately executed. For
example, WATFIV is a compiler that produces the absolute machine code
as output.
Relocatable machine language as an output allows subprograms and
subroutines to be compiled separately. Relocatable object modules can be
linked together and loaded by a linking loader. But there is added expense
of linking and loading.
Assembly language as output makes the code generation easier. We can
generate symbolic instructions and use the macro-facilities of assemblers
in generating code. And we need an additional assembly step after code
generation.
Memory Management
Mapping the names in the source program to the addresses of data objects is done
by the front end and the code generator. A name in the three address statements refers to
the symbol table entry for the name. Then from the symbol table entry, a relative address
can be determined for the name.
Instruction selection
Selecting the best instructions will improve the efficiency of the program. It
includes the instructions that should be complete and uniform. Instruction speeds and
machine idioms also play a major role when efficiency is considered. But if we do not
care about the efficiency of the target program then instruction selection is
straightforward. For example, the respective three-address statements would be
translated into the latter code sequence as shown below:
P:=Q+R
S:+P+T
MOV Q, R0
ADD R, R0
MOV R0, P
MOV P, R0
ADD T, R0
MOV R0, S
Here the fourth statement is redundant as the value of the P is loaded again in that
statement that just has been stored in the previous statement. It leads to an inefficient
code sequence. A given intermediate representation can be translated into many code
sequences, with significant cost differences between the different implementations. Prior
knowledge of instruction cost is needed in order to design good sequences, but accurate
cost information is difficult to predict.
Register allocation issues
To understand the concept consider the following three address code sequence
t:=a+b
t:=t*c
t:=t/d
MOV a.R0
ADD b.R0
MUL c.R0
DIV d.R0
MOV R0,t
Evaluation order
The code generator decides the order in which the instruction will be executed.
The order of computations affects the efficiency of the target code. Among many
computational orders, some will require only fewer registers to hold the intermediate
results. However, picking the best order in the general case is a difficult NP- complete
program.
Approaches to code generation issues:
Code generator must always generate the correct code. It is essential because of the
number of special cases that a code generator might face. Some of the design goals of
code generator are:
Correct
Easily maintainable
Testable
Efficient
Disadvantages in the design of a code generator:
Limited flexibility:
Code generators are typically designed to produce a specific type of code, and as a
result, they may not be flexible enough to handle a wide range of inputs or generate code
for different target platforms. This can limit the usefulness of the code generator in
certain situations.
Maintenance overhead:
Performance issues:
Depending on the complexity of the code being generated, a code generator may
not be able to generate optimal code that is as performant as hand-written code. This can
be a concern in applications where performance is critical.
Learning curve:
Code generators can have a steep learning curve, as they typically require a deep
understanding of the underlying code generation framework and the programming
languages being used. This can make it more difficult to on board new developers onto a
project that uses a code generator.
Over-reliance:
It’s important to ensure that the use of a code generator doesn’t lead to
over-reliance on generated code, to the point where developers are no longer able to write
code manually when necessary. This can limit the flexibility and creativity of a
development team, and may also result in lower quality code overall.
PART – C (1 x 15 = 15 Marks)
16. a) Explain in detail about optimization of Basic Blocks and construct the DAG for
the following Basic Block.
(i) t1: =4*I,
(ii) t2: =a[t1],
(iii) t3: =4*I,
(iv) t4: =b[t3],
(v) t5: =t2*t4,
(vi) t6: =prod+t5,
(vii) prod: =t6,
(viii) t7: =i+1,
(ix) i: =t7,
(x) if i<=20 goto (1). (15)
(OR)
16. b) Develop an efficient data flow graph and algorithm with example. (15)
Data Flow Graph (DFG)
A Data Flow Graph (DFG) is a graphical representation of the data dependencies
between operations in a program. In a DFG, nodes represent operations or computations,
and edges represent the flow of data between these operations. DFGs are used for
optimizing and parallelizing code, especially in the context of compiler design and digital
signal processing.
Developing an Efficient Data Flow Graph
Consider the following sequence of operations:
1. t1 = a + b
2. t2 = t1 * c
3. t3 = a + b
4. t4 = t3 - d
5. t5 = t2 + t4
Steps to Create an Efficient DFG
1. Identify Operations and Data Dependencies: Determine the operations and their
dependencies.
2. Eliminate Redundant Computations: Use common subexpression elimination to avoid
duplicate computations.
3. Construct Nodes and Edges: Create nodes for each operation and edges to represent data
dependencies.
4. Optimize Node Placement: Arrange nodes to highlight parallelism and data reuse.