Unit Iii QB
Unit Iii QB
Part- A
1. Write the semantic action for the production rule of E ->E1 OR ME2. (Nov/Dec
2020)
{X1.type=X2.type}
E1.type=E2.type
X1=E1;X2=E2
2. Translate the arithmetic expression x = (a + b)∗– c/d into quadruples and triples.
(Nov/Dec 2020)
t1=-c
t2=t1/d
t3=a+b
t4=t3*t2
x=t4
Quadruples
op arg1 arg2 Result
- c t1
/ t1 d t2
+ a b t3
* t3 t2 t4
= t4 x
Triples
op arg1 arg2
- c
/ (1) d
+ a b
* (3) (2)
= (4) x
= (6)
7. Convert the following statement into three address codes. (April/May 2022)
x=a+(b*-c)+(d*-e)
Represent the three address code in triples.
t1=-c
t2=-e
t3=b*t1
t4=d*t2
t5=t3+t4
t6=a+t5
x=t6
op arg1 arg2
- c
- e
* b (1)
* d (2)
+ (1) (2)
+ a (5)
= x (6)
8. Give syntax directed translation for the following statement (Nov/Dec 2020)
Call sum(int a, int b).
S-> call id (Elist) — generate a param statement for each item on queue, causing these
statements to follows the statements evaluating the argument expressions { for each
item p on queue do emit (`param' P);
Emit(‘call’id.place)}
Elist-* Elist, E {append E. place to the end of the queue}
Elist --> E {initialize queue to contain only E. place}
12. Write a grammar for flow control statement while-do. (April/May 2019)
S. BEGIN = newlabel;
E. TRUE = newlabel;
E. FALSE = S. NEXT;
S1. NEXT = S. BEGIN;
S. CODE = GEN(S. BEGIN '− ') | |
E. CODE | | GEN(E. TRUE '− ')| |
S1. CODE | | GEN('goto' S. BEGIN)
13. Write three address code sequence for the assignment Statement. (Nov/Dec 2021)
d := (a −b) + (a − c) + (a − c) .
t1=a-b
t2=a-c
t3=t2+t2
t4=t1+t3
d=t4
15. Determine the types and relative addresses for the identifiers in the following
sequence of declarations: (April/May 2023)
float x;
record{float x;float y;}p;
record{int tag;float x;float y;}q;
16. Write down syntax directed definition for simple desk calculator. (Nov/Dec 2016)
Production Semantic rules
2. Specify a type checker which can handle expressions, statements and functions.
(Nov/Dec 2020)
Specify a type checker for a simple language in which the type of each identifier must
be declared before the identifier is used. The type checker is a translation scheme that
synthesizes the type of each expression from the types of its sub expressions. The type
checker can handle arrays, pointers, statements and functions.
A simple language
Type checking of expression
Type checking of statement
Type checking of 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) }
3. What are the rules for type checking? Give an example. (Nov/Dec 2020) (April/May
2019
Type checking is the process of verifying and enforcing constraints of types in
values. A compiler must check that the source program should follow the syntactic and
semantic conventions of the source language and it should also check the type rules of
the language. It allows the programmer to limit what types may be used in certain
circumstances and assigns types to values. The type-checker determines whether these
values are used appropriately or not.
It checks the type of objects and reports a type error in the case of a violation,
and incorrect types are corrected. Whatever the compiler we use, while it is compiling
the program, it has to follow the type rules of the language. Every language has its own
set of type rules for the language. We know that the information about data types is
maintained and computed by the compiler.
The information about data types like INTEGER, FLOAT, CHARACTER, and
all the other data types is maintained and computed by the compiler. The compiler
contains modules, where the type checker is a module of a compiler and its task is type
checking.
Conversion
Conversion from one type to another type is known as implicit if it is to be
done automatically by the compiler. Implicit type conversions are also
called Coercion and coercion is limited in many languages.
Example: An integer may be converted to a real but real is not converted to an integer.
Conversion is said to be Explicit if the programmer writes something to do the
Conversion.
Tasks:
1. has to allow “Indexing is only on an array”
2. has to check the range of data types used
3. INTEGER (int) has a range of -32,768 to +32767
4. FLOAT has a range of 1.2E-38 to 3.4E+38.
Types of Type Checking:
There are two kinds of type checking:
1. Static Type Checking.
2. Dynamic Type Checking.
Static Type Checking:
Static type checking is defined as type checking performed at compile time. It
checks the type variables at compile-time, which means the type of the variable is
known at the compile time. It generally examines the program text during the
translation of the program. Using the type rules of a system, a compiler can infer from
the source text that a function (fun) will be applied to an operand (a) of the right type
each time the expression fun(a) is evaluated.
Examples of Static checks include:
Type-checks: A compiler should report an error if an operator is applied to an
incompatible operand. For example, if an array variable and function variable are
added together.
The flow of control checks: Statements that cause the flow of control to leave a
construct must have someplace to which to transfer the flow of control. For
example, a break statement in C causes control to leave the smallest enclosing
while, for, or switch statement, an error occurs if such an enclosing statement does
not exist.
Uniqueness checks: There are situations in which an object must be defined only
once. For example, in Pascal an identifier must be declared uniquely, labels in a
case statement must be distinct, and else a statement in a scalar type may not be
represented.
Name-related checks: Sometimes the same name may appear two or more times.
For example in Ada, a loop may have a name that appears at the beginning and end
of the construct. The compiler must check that the same name is used at both places.
The Benefits of Static Type Checking:
1. Runtime Error Protection.
2. It catches syntactic errors like spurious words or extra punctuation.
3. It catches wrong names like Math and Predefined Naming.
4. Detects incorrect argument types.
5. It catches the wrong number of arguments.
6. It catches wrong return types, like return “70”, from a function that’s declared to
return an int.
Dynamic Type Checking:
Dynamic Type Checking is defined as the type checking being done at run time.
In Dynamic Type Checking, types are associated with values, not variables.
Implementations of dynamically type-checked languages runtime objects are generally
associated with each other through a type tag, which is a reference to a type containing
its type information. Dynamic typing is more flexible. A static type system always
restricts what can be conveniently expressed. Dynamic typing results in more compact
programs since it is more flexible and does not require types to be spelled out.
Programming with a static type system often requires more design and implementation
effort.
The design of the type-checker depends on:
1. Syntactic Structure of language constructs.
2. The Expressions of languages.
3. The rules for assigning types to constructs (semantic rules).
1. Call by Values
When using call by value, the compiler adds the r-value of the actual parameters
that were passed to the calling procedure to the called procedure's activation record.
Any modifications made to the formal parameters do not affect the actual parameters
because they include the values given by the calling procedure.
Program
#include <bits/stdc++.h>
using namespace std;
// Formal parameter
void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
int main() {
// Actual parameter
int x = 9;
int y = 12;
cout << "Values before swap: x= " << x << " y= " << y << endl;
swap(x, y);
cout << "Values after swap: x= " << x << " y= " << y << endl;
}
Output
Values before swap: x=9 y=12
Values after swap: x=9 y=12
Explanation
As we can see, even when we modify the contents of variables x and y defined
in the scope of the swap function, the modifications do not affect the variables'
definitions in the scope of the main. This is because main() will not be affected by
changes performed in the swap() because we call swap() by value, which will receive
separate memory for x and y.
2. Call by Reference
The formal and actual parameters in a call by reference relate to the same
memory address. The activation record of the called function receives a copy of the L-
value of the actual arguments. As a result, the address of the actual parameters is passed
to the called function.
If the actual parameters do not contain an L-value, they are evaluated in a new
temporary location, and the location's address is passed. Since changes are made at the
address, any modifications to the formal parameter are reflected in the actual
parameters.
Program
#include <stdio.h>
// Call by reference
// Formal parameter
void swap(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}
int main() {
// Actual parameter
int x = 9;
int y = 12;
printf("Values before swap: x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("Values after swap: x = %d, y = %d", x, y);
}
Output
Values before swap: x=9 y=12
Values after swap: x=12 y=9
Explanation
As we can see, instead of int x, int y, we used int *x, int y, and instead of giving
x,y, we gave &x,&y in the function call. Due to the usage of pointers as function
parameters, which return the original parameters' address rather than their value, this
practice is called by reference.
The variables' addresses are given using the & operator, and the memory
location to which the pointer is pointing is accessed using the * operator. The
modifications done in the swap() reflect in main(), as shown in the output above
because the variable function points to the same memory address as the original
arguments.
Output
The value of a is: 5
Explanation
Value is passed into the function in the example above, but until the function is
complete, it has no impact on the value of the passed-in variable. At that time, the
function variable's final value is saved in the passed-in variable.
4. Call by Name
A new form of preprocessor-like argument parsing mechanism is offered by
languages like Algol. Here, the procedure's body is called instead of the procedure's
name.
Program
#include <bits/stdc++.h>
using namespace std;
int a, b, c;
// Formal parameter
void swap(int a, int b) {
int t;
// These three swapping is done by compiler
t = a;
a = b;
b = t;
cout << a << " " << b << endl;
}
int main() {
// Actual parameter
int a = 9;
int b = 12;
cout << a << " " << b << endl;
//cout<<"Values before swap: x= "<<x<<" y= "<<y<<endl;
swap(a, b);
cout << a << " " << b;
//cout<<"Values after swap: x= "<<x<<" y= "<<y<<endl;
}
Output
9 12
12 9
9 12
Explanation
Here, the evaluation is done based on parameters. In all cases where formal
parameters are used in the technique, actual parameters are used instead of formal ones.
5. Suppose that we have a production A->BCD. Each of the four non terminals have two
attributes:s is a synthesized attribute, and i is an inherited attribute. For each of the sets
of rules below, tell whether (i) the rules are consistent with an S-attributed definition
(ii) the rules are consistent with an L-attributed definition (iii) whether the rules are
consistent with any evaluation order at all? (Nov/Dec 2018)
(1) A.s=B.i+C.i
(2) A.s=B.i+C.s and D.i=A.i+B.s
(3) A.s=B.s+D.s
(4) A.s=D.i
B.i=A.s+C.s
C.i=B.s
D.i=B.i+C.i
(1) This is L attributed SDD. Attributes of parent node can take values from their
children
(2) A cannot have inherited attribute. Since, there is nothing present on the LHS of A.
So this SDD is neither S attributed nor L attributed
(3) A's synthesized attribute is a function of synthesized attributes of its children. This
confirms to S attributed definition. Every S attributed SDD is also L attributed SDD
(4) In the rule B.i=A.s+C.s. Here B's inherited attributed is taking values from its right
sibling C. This violates L-attributed definition which says that inherited attributes are
limited to take values from its parents or left siblings only. Hence, this SDD is not L-
attributed.
6. Describe how SDD can be evaluated at the nodes of a parse tree using dependency
graphs. (Nov/Dec 2020)
Evaluation order for SDD includes how the SDD(Syntax Directed Definition)
is evaluated with the help of attributes, dependency graphs, semantic rules, and S and
L attributed definitions. SDD helps in the semantic analysis in the compiler so it’s
important to know about how SDDs are evaluated and their evaluation order. This
article provides detailed information about the SDD evaluation. It requires some basic
knowledge of grammar, production, parses tree, annotated parse tree, synthesized and
inherited attributes.
Terminologies:
Parse Tree: A parse tree is a tree that represents the syntax of the production
hierarchically.
Annotated Parse Tree: Annotated Parse tree contains the values and attributes at
each node.
Synthesized Attributes: When the evaluation of any node’s attribute is based on
children.
Inherited Attributes: When the evaluation of any node’s attribute is based on
children or parents.
Dependency Graphs:
A dependency graph provides information about the order of evaluation of
attributes with the help of edges. It is used to determine the order of evaluation of
attributes according to the semantic rules of the production. An edge from the first node
attribute to the second node attribute gives the information that first node attribute
evaluation is required for the evaluation of the second node attribute. Edges represent
the semantic rules of the corresponding production.
Dependency Graph Rules: A node in the dependency graph corresponds to the node
of the parse tree for each attribute. Edges (first node from the second node)of the
dependency graph represent that the attribute of the first node is evaluated before the
attribute of the second node.
Production Table
3. A1 ⇢ B A1.syn = B.syn
Node Attribute
1 digit.lexval
2 digit.lexval
3 digit.lexval
4 B.syn
5 B.syn
6 B.syn
7 A1.syn
8 A.syn
9 A1.inh
10 S.val
S-Attributed Definitions:
S-attributed SDD can have only synthesized attributes. In this type of definitions
semantic rules are placed at the end of the production only. Its evaluation is based on
bottom up parsing.
Example: S ⇢ AB { S.x = f(A.x | B.x) }
L-Attributed Definitions:
L-attributed SDD can have both synthesized and inherited (restricted inherited
as attributes can only be taken from the parent or left siblings). In this type of definition,
semantics rules can be placed anywhere in the RHS of the production. Its evaluation is
based on inorder (topological sorting).
Example: S ⇢ AB {A.x = S.x + 2} or S ⇢ AB { B.x = f(A.x | B.x) } or S ⇢
AB { S.x = f(A.x | B.x) }
Note:
Every S-attributed grammar is also L-attributed.
For L-attributed evaluation in order of the annotated parse tree is used.
For S-attributed reverse of the rightmost derivation is used.
Semantic Rules with controlled side-effects:
Side effects are the program fragment contained within semantic rules. These
side effects in SDD can be controlled in two ways: Permit incidental side effects and
constraint admissible evaluation orders to have the same translation as any admissible
order.
Table-2
Edge
Corresponding Semantic Rule
From To (From the production table)
1 4 B.syn = digit.lexval
2 5 B.syn = digit.lexval
3 6 B.syn = digit.lexval
4 7 A1.syn = B.syn
8 9 A1.inh = A.syn
7. Write the syntax directed translation and parse tree for the following code (April/May
2022)
S->id:=E
E->E1+E2
E->E1*E2
E->E1
E->(E1)
E->id
The translation scheme of above grammar is given below:
S → id :=E {p = look_up(id.name);
If p ≠ nil then
Emit (p = E.place)
Else
Error;
}
E → E1 + E2 {E.place = newtemp();
Emit (E.place = E1.place '+' E2.place)
}
E → E1 * E2 {E.place = newtemp();
Emit (E.place = E1.place '*' E2.place)
}
E → id {p = look_up(id.name);
If p ≠ nil then
Emit (p = E.place)
Else
Error;
}
8. What is a symbol table? What type of information is stored in it? Discuss on the use of
data structures (i) arrays (ii) linked lists (iii) Binary search trees for implementing
symbol table. (Nov/Dec 2019) (April/May 2023)
The symbol table is defined as the set of Name and Value pairs. Symbol Table
is an important data structure created and maintained by the compiler in order to keep
track of semantics of variables i.e. it stores information about the scope and binding
information about names, information about instances of various entities such as
variable and function names, classes, objects, etc.
Items stored in Symbol table:
Variable names and constants
Procedure and function names
Literal constants and strings
Compiler generated temporaries
Labels in source languages
Information used by the compiler from Symbol table:
Data type and name
Declaring procedures
Offset in storage
If structure or record then, a pointer to structure table.
For parameters, whether parameter passing by value or by reference
Number and type of arguments passed to function
Base Address
Implementation of Symbol table
1. Array
Single array or equivalently several arrays was used, to store names and their
associated information, new names are added to the list in the order in which they are
encountered. The position of the end of the array is marked by the pointer available,
pointing to where the next symbol-table entry will go. The search for a name proceeds
backwards from the end of the array to the beginning. When the name is located the
associated information can be found in the words following next.
2. Linked list
This implementation is using a linked list. A link field is added to each record.
Searching of names is done in order pointed by the link of the link field.
A pointer “First” is maintained to point to the first record of the symbol table.
Insertion is fast O(1), but lookup is slow for large tables – O(n) on average
Example : The grammar in is LL( 1 ) and hence suitable for tap-down parsing
can be generated by predicting a suitable production rule.
E E1 + T { E.nptr := mknode('+', Enptr, T.nptr) }
E E1 - T { E.nptr := mkrtodr('-', El.nptr, T.nptr)}
E T { E.nptr := T.nptr }
E R { E.nptr := R.nptr } R ε
T id {T.nptr :=mkleaf(id, id.entry)}
T num {T.nptr :=mkleaf(num, num.entry)}
Combine two of the E-productions to make the translator smaller. The new
productions use token op to represent + and -.
E E1 op T { E.nptr := mknode('op', Enptr, T.nptr) }
E T { E.nptr := T.nptr }
E R { E.nptr := R.nptr } R ε
T id {T.nptr :=mkleaf(id, id.entry)}
T num {T.nptr :=mkleaf(num, num.entry)}
10. Discuss the address code and its implementations with example. (Nov/Dec 23)
Three address code is implemented as records with address fields.
1. Quadruples
2. Triples
3. Indirect Triples
Quadruples
In quadruples representation, each instruction is splitted into the following 4 different
fields-
op, arg1, arg2, result
Here-
• The op field is used for storing the internal code of the operator.
• The arg1 and arg2 fields are used for storing the two operands used.
• The result field is used for storing the result of the expression.
Triples
In triples representation,
• References to the instructions are made.
• Temporary variables are not used.
Indirect Triples
This representation is an enhancement over triples representation.
It uses an additional instruction array to list the pointers to the triples in the
desired order.
Thus, instead of position, pointers are used to store the results.
It allows the optimizers to easily re-position the sub-expression for producing
the optimized code.
Example
a = b * – c + b* – c
Three Address Code
T1 = uminus c
T2 = b x T1
T3 = uminus c
T4 = b x T3
T5 = T2 + T4
a = T5
Quadruple Representation-
Location Op Arg1 Arg2 Result
(1) uminus c T1
(2) * b T1 T2
(3) uminus c T3
(4) x b T3 T4
(5) + T2 T4 T5
(6) = T5 a
Triple Representation-
Location Op Arg1 Arg2
(1) uminus c
(2) x b (1)
(3) uminus c
(4) x b (3)
(6) = a (5)
Indirect Triples
Location Statement
1000 (1)
1001 (2)
1002 (3)
1003 (4)
1004 (5)
1005 (6)