Guru Tegh Bahadur Institute of Technology, Delhi
(Affiliated to Guru Gobind Singh Indraprastha University, Dwarka, New Delhi)
Department of Computer Science & Engineering
PRACTICAL FILE
Compiler Design Lab
(Paper code: CIC 351)
Submitted to: Submitted by:
MS. ESHA SAXENA Name: ASHMEET KAUR
Assistant Professor Class: CSE-03(5th sem)
Enroll. No.: 09576802722
Index
S.No. Name of Experiment Date T.Sign
1
10
11
EXPERIMENT-01
Aim:
Practice of LEX/YACC of Compiler design.
Theory:
1) LEX (Lexical Analyzer Generator)
Purpose:
LEX is a lexical analyzer generator that takes a specification of tokens (patterns of
characters, like keywords, operators, identifiers, etc.) and generates a lexical
analyzer (or scanner). The lexical analyzer reads the input, identifies tokens, and
passes them to the parser (generated by YACC).
How LEX Works:
LEX works by specifying patterns using regular expressions. Each regular
expression describes a pattern of characters that make up a token. LEX translates
these patterns into C code for scanning text.
1. Input: A specification file containing regular expressions and actions to
perform when these patterns are matched.
2. Output: C code that implements a lexical analyzer.
3. Execution: The generated lexical analyzer reads the input stream, identifies
tokens by matching them against the patterns, and returns the tokens to the
parser.
Structure of a LEX Program:
A typical LEX program is divided into three sections:
1. Definition Section: Used for defining variables, constants, or libraries.
2. Rules Section: Contains regular expressions and associated actions.
3. User Code Section: Contains user-defined helper functions, typically in C.
Advantages of LEX:
• Automated Lexical Analysis: It simplifies the process of tokenizing input by
using regular expressions to automatically generate C code for token
recognition.
• Efficiency: LEX-generated scanners are highly efficient and fast.
2) YACC (Yet Another Compiler-Compiler)
Purpose:
YACC is a parser generator that reads a formal grammar specification and
generates C code for a parser. The parser processes tokens (provided by the lexical
analyzer) and constructs the syntactic structure of the input (typically in the form of
a parse tree or abstract syntax tree).
How YACC Works:
YACC takes a context-free grammar as input and generates a bottom-up parser
(typically a LALR(1) parser). It processes tokens provided by the lexical analyzer
(LEX) and applies grammar rules to determine if the input sequence of tokens is
syntactically valid.
1. Input: A specification file that defines the grammar rules.
2. Output: C code that implements the syntax analyzer (parser).
3. Execution: The parser reads tokens, applies grammar rules, and reports
syntax errors or builds an abstract syntax tree (AST).
Structure of a YACC Program:
A typical YACC program is divided into three sections:
• Definition Section: Declarations of tokens and types.
• Rules Section: Grammar rules, each with an associated action.
• User Code Section: Auxiliary C functions and code.
Advantages of YACC:
• Automated Parser Generation: YACC automates the process of writing
parsers, making it easier to design complex grammars.
• Error Detection: YACC can handle syntax errors efficiently by reporting
errors when tokens do not match grammar rules.
EXPERIMENT-02
Aim:
Write a program to check whether a string include keyword or not.
Code:
#include<iostream>
#include<string>
int main() {
std::string str, keyword;
std::cout << "Enter a string: ";
std::getline(std::cin, str);
std::cout << "Enter a keyword: ";
std::getline(std::cin, keyword);
if (str.find(keyword) != std::string::npos) {
std::cout << "The keyword is present in the string." << std::endl;
} else {
std::cout << "The keyword is not present in the string." << std::endl;
}
return 0;
}
Output:
EXPERIMENT-03
Aim:
Write a program to check whether a string contains an alphabet or not.
Code:
#include <iostream>
#include <string>
#include <cctype> // For isalpha function
int main() {
std::string str;
bool hasAlphabet = false;
std::cout << "Enter a string: ";
std::getline(std::cin, str);
for (char c : str) {
if (isalpha(c)) {
hasAlphabet = true;
break;
}
}
if (hasAlphabet) {
std::cout << "The string contains at least one alphabet character." << std::endl;
} else {
std::cout << "The string does not contain any alphabet characters." <<
std::endl;
}
return 0;
}
Output:
EXPERIMENT-04
Aim:
Write a program to show all the operations of a stack.
Code:
#include <iostream>
#include <stack>
int main() {
std::stack<int> stack;
int choice, value;
do {
std::cout << "\nStack Operations Menu:";
std::cout << "\n1. Push";
std::cout << "\n2. Pop";
std::cout << "\n3. Top";
std::cout << "\n4. Is Empty";
std::cout << "\n5. Size";
std::cout << "\n6. Exit";
std::cout << "\nEnter your choice: ";
std::cin >> choice;
switch (choice) {
case 1:
std::cout << "Enter value to push: ";
std::cin >> value;
stack.push(value);
std::cout << value << " pushed into the stack." << std::endl;
break;
case 2:
if (!stack.empty()) {
std::cout << "Popped value: " << stack.top() << std::endl;
stack.pop();
} else {
std::cout << "Stack is empty." << std::endl;
}
break;
case 3:
if (!stack.empty()) {
std::cout << "Top value: " << stack.top() << std::endl;
} else {
std::cout << "Stack is empty." << std::endl;
}
break;
case 4:
if (stack.empty()) {
std::cout << "Stack is empty." << std::endl;
} else {
std::cout << "Stack is not empty." << std::endl;
}
break;
case 5:
std::cout << "Stack size: " << stack.size() << std::endl;
break;
case 6:
std::cout << "Exiting..." << std::endl;
break;
default:
std::cout << "Invalid choice. Please try again." << std::endl;
break;
}
} while (choice != 6);
return 0;
}
Output:
EXPERIMENT-05
Aim:
Write a program to remove left recursion from a grammar.
Code:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// A structure to represent a production rule
struct Production {
char nonTerminal;
vector<string> symbols;
};
// Function to remove left recursion
vector<Production> removeLeftRecursion(const vector<Production> &
productions) {
vector<Production> newProductions;
for (const Production& prod : productions) {
vector<string> newSymbols;
vector<string> newSymbolsRec;
// Group symbols into recursive and non-recursive
for (const string&symbol: prod.symbols){
if (symbol[0]==prod.nonTerminal){
newSymbolsRec.push_back(symbol.substr(1));
} else{
newSymbols.push_back(symbol);
}
}
// If there's left recursion, modify the production
if(!newSymbolsRec.empty()){
char newNonTerminal = prod.nonTerminal+1;
newProductions.push_back({prod.nonTerminal, newSymbols});
vector<string> newRecSymbols;
for(const string& recSymbol:newSymbolsRec){
newRecSymbols.push_back(recSymbol+newNonTerminal);
}
newRecSymbols.push_back(""); //Add an epsilon production
newProductions.push_back({newNonTerminal, newRecSymbols});
}else{
newProductions.push_back(prod);
}
}
return newProductions;
}
int main(){
vector<Production> productions={
{'E',{"E+T","T"}},
{'T',{"T*F","F"}},
{'F',{"(E)","id"}}
};
vector<Production> newProductions=removeLeftRecursion(productions);
cout<<"Original Productions:"<<endl;
for(const Production& prod:productions){
cout<<prod.nonTerminal<<".>";
for(const string& symbol:prod.symbols){
cout<<symbol<<"|";
}
cout<<endl;
}
cout<<"\nProductions after removing left recursion:"<<endl;
for(const Production& prod:newProductions){
cout<<prod.nonTerminal<<".>";
for(const string&symbol:prod.symbols){
cout<<symbol<<"|";
}
cout<<endl;
}
return 0;
}
Output: