##1. Enunt
Sa se implementeze in limbajul C/C++ un program care sa realizeze operatia Traducere in C pentru un limbaj de programare minimalist ce poate contine urmatoarele elemente:
0 – Comentarii: # Acesta e un comentariu (mono-linie). Tot ce e de la simbolul '#' pana la finalul liniei e ignorat.
1 – Definire de variabila: var nume_variabila
2 – Definire de vector: var nume_vector[numar_elemente]
3 – Atribuire: Variabila = Expresie, unde:
- Variabila poate fi
nume_variabilasaunume_vector[index_element] - Expresie e orice expresie aritmetica valida ce poate contine operatorii aritmetici: +, -, (, ) si drept operanzi poate contine variabile (
nume_variabilasaunume_vector[index_element]) sau valori numerice (reprezentand numere intregi pozitive scrise in baza 10).
4 – Decizie:
if Conditie
Instructiuni
else
Instructiuni
end
unde:
- Conditie este o expresie ce poate sa contina variabile sau valori numerice si operatorii relationali > < = si operatorul logic | (sau),
- Instructiuni e orice succesiune (inclusiv vida) de instructiuni (dintre acestea pe care le recunoaste limbajul).
5 – Repetitie:
while Conditie
Instructiuni
end
6 – Afisare: print Variabila sau: print ”orice sir de caractere inclus intre ghilimele, inclusiv \n pentru 'linie noua'...”
7 – Citire: read Variabila
Variabilele din program (atat cele scalare, cat si elementele din vectori) vor fi de tipul intreg. Toti operatorii vor avea asociativitate la stanga, iar operatorii relationali vor avea precedenta mai mare decat operatorul logic.
##2. Prezentare generala
Programul este compus din 3 clase : Lexer, AST si Translator
###Lexer
Clasa Lexer are două atribute: o variabilă de tip string numită script_, în care este salvat textul ce urmează a fi parsat, și o altă variabilă de tip int numită pos_, în care se păstrează poziția curentă în text.
Clasa Lexer se ocupă de transformarea input-ului (fișierul text) într-un vector de Tokens.
Un Token este reprezentat de o structură care are următoarele atribute: type, value și line.
-
typetypeeste de tipulTokenType.TokenTypeeste un enum care cuprinde toate tipurile de tokens, de exemplu:VAR,IDENTIFIER,NUMBER,IF,WHILE, etc.
-
valuevalueeste de tipul string pentruTokenscare au o anumită valoare, de exemplu:STRING,NUMBER.
-
linelineeste de tipul int și reprezintă linia din fișier pe care se regăsește token-ul.
Constructorul clasei Lexer primește ca parametru un string, care mai apoi este parcurs pentru a crea vectorul de tip Token.
Funcția parse din clasa Lexer se ocupă de parcurgerea textului și generarea vectorului de Tokens. Iată cum funcționează această funcție:
-
Inițializare: Se inițializează un vector gol de
Token-uri și variabilalinecare reprezintă numărul liniei curente. -
Parcurgere: Folosind o buclă
while, funcția parcurge fiecare caracter din text până la sfârșitul acestuia (pos_ < script_.size()). -
Verificare linie nouă (
'\n'): Dacă caracterul curent este o linie nouă ('\n'), se incrementează variabilaline. -
Comentarii (
'#'): Dacă caracterul curent este'#', se apelează funcțiaskipLinepentru a sări peste întreaga linie. -
Șiruri de caractere (
'"'): Dacă caracterul curent este'"', se apelează funcțiaparseStringpentru a extrage și adăuga unTokende tipSTRINGîn vectorultokens. -
Spații albe (
isspace): Dacă caracterul curent este un spațiu alb, acesta este ignorat prin incrementarea variabileipos_. -
Numere (
isdigit): Dacă caracterul curent este un număr, se apelează funcțiaparseNumberpentru a extrage și adăuga unTokende tipNUMBERîn vectorultokens. -
Cuvinte cheie sau identificatori (
isalpha): Dacă caracterul curent este o literă, se apelează funcțiaparseKeywordOrIdentifierpentru a extrage și adăuga unTokencorespunzător în vectorultokens. -
Caractere speciale (
switch): Dacă caracterul curent este altceva decât cele menționate mai sus, se folosește o structurăswitchpentru a gestiona caracterele speciale precum +, -, *, /, <, >, ,, (, ), [, ], &, |, =. Se apelează funcții specifice pentru anumite simboluri și se adaugăToken-urile corespunzătoare în vectorultokens. -
EOF Token: La final, se adaugă un
Tokende tipEOF_TOKENpentru a marca sfârșitul fluxului deTokens. -
Returnare: Se returnează vectorul complet de
Tokens.
Această funcție parcurge și analizează fiecare caracter din text, generând un vector structurat de Tokens care pot fi folosite pentru analiza sintactică și execuția programului asociat.
În general, pentru a determina dacă o porțiune de text reprezintă un atom lexical, se examinează prima literă a acesteia pentru a identifica categoria posibilă. Ulterior, textul este parcurs până când există doar o variantă posibilă, funcțiile precum parseNumber sau parseString aplicând această logică.
De exemplu, în cazul funcției parseKeywordOrIdentifier, unde atomul lexical începe cu o literă, se inițiază o buclă while care parcurge textul de la poziția curentă, generând un șir de caractere (value) până când următorul caracter nu mai este o literă. Există și verificări pentru simbolurile _ și -, care pot fi prezente în numele unei variabile. Dacă ultimul caracter este -, atomul lexical nu poate exista, deoarece nu există niciun tip de atom lexical care să corespundă acestui format.
Apoi, printr-un set de instrucțiuni if-else se verifică dacă șirul value corespunde cu tipurile de atomi lexicali precum var, if, while, end, print, read. În cazul în care nu corespunde cu niciunul dintre acestea, tipul acestui atom lexical este considerat un identifier, adică un nume de variabilă, având în vedere că s-au verificat respectarea regulilor pentru numele unei variabile.
În final, funcția returnează atomul lexical generat, iar poziția este actualizată pentru a reflecta progresul în parcurgerea textului.
Token Lexer::parseKeywordOrIdentifier(int line)
{
std::string value;
while (isalnum(script_[pos_]) || script_[pos_] == '_' || script_[pos_] == '-')
{
value += script_[pos_++];
}
if (value.back() == '-')
{
std::cerr << "[" << line << "]" << "Unexpected character ( - )";
return {};
}
// Check if the identifier is a keyword
if (value == "var")
return {VAR, value, line};
else if (value == "if")
return {IF, value, line};
else if (value == "while")
return {WHILE, value, line};
else if (value == "print")
return {PRINT, value, line};
else if (value == "read")
return {READ, value, line};
else if (value == "else")
return {ELSE, value, line};
else if (value == "end")
return {END, value, line};
else
return {NAME, value, line}; // Assume it's an identifier if not a keyword
}Clasa AST are două atribute principale: o variabilă de tip string numită tokens_, unde este stocat vectorul de atomi lexicali ce va fi parsat, și o altă variabilă de tip int numită pos_, responsabilă cu urmărirea poziției curente în vector.
Clasa AST se ocupă de transformarea input-ului (vectorul de atomi lexicali) într-un Arbore de Sintaxă Abstractă (AST), construit din structuri de tip ASTNode.
Un ASTNode este reprezentat printr-o structură ce conține următoarele atribute: type, value, și children.
-
typetypeeste de tipulASTNodeType.ASTNodeTypeeste un enum care include toate tipurile de noduri specifice Arborelui de Sintaxă Abstractă. De exemplu:VARIABLE_DECLARATION(Declarație de Variabilă),ASSIGNMENT(Atribuire),IF_STATEMENT(Instrucțiune IF),WHILE_LOOP(Buclă WHILE),PLUS_OPERATION(Operație de Adunare), etc.
ASTNodeType furnizează o modalitate eficientă de etichetare și distingere a diferitelor tipuri de noduri într-un AST, ușurând astfel analiza și interpretarea programelor. Fiecare tip de nod corespunde unei construcții sintactice specifice în limbajul de programare respectiv.
-
valuevalueeste de tipul string pentru nodurile care au o valoare asociată.
-
childrenchildreneste de tipul vector deASTNodeși reprezintă copiii nodului curent.
AST(const std::vector<Token> &tokens): tokens_(tokens), pos_(0) {}
void AST::parse(ASTNode &Program)
{
while (pos_ < tokens_.size())
{
switch (current().type)
{
case TokenType::VAR:
Program.children.push_back(parseDeclaration());
break;
case TokenType::NAME:
Program.children.push_back(parseAssignment());
break;
case TokenType::PRINT:
Program.children.push_back(parsePrint());
break;
case TokenType::READ:
Program.children.push_back(parseRead());
break;
case TokenType::WHILE:
Program.children.push_back(parseWhile());
break;
case TokenType::IF:
Program.children.push_back(parseIf());
break;
default:
consume();
break;
}
}
}Constructorul:
- Constructorul clasei
ASTprimește un vector de atomi lexicalitokensși îl atribuie variabilei membretokens_. De asemenea, inițializeazăpos_cu 0 pentru a marca poziția curentă în vector.
Funcția Parse:
-
Funcția
parseare ca responsabilitate construirea Arborele de Sintaxă Abstractă (AST). În timpul execuției, funcția parcurge vectorul de atomi lexicali, analizându-i pentru a identifica tipul de nod potențial.- Se examinează atomii lexicali pentru a identifica tipul de nod posibil.
- Vectorul este parcurs în continuare pentru a identifica exact acel tip de nod.
- La finalul acestui proces, funcția returnează un obiect de tip
ASTNode, care reprezintă nodul construit pe baza atomilor lexicali analizați.
-
Vom urmări un exemplu specific când funcția identifică un atom lexical de tip
VARși apelează funcțiaparseDeclarationpentru a continua procesul de analiză și construcție a nodului corespunzător.
#####Funcția parseDeclaration
Funcția parseDeclaration are rolul de a analiza și structura unui token care declară o variabilă (var). Iată cum funcționează:
-
Crearea Nodului Variabilei (
varNode):- Se creează un obiect
ASTNodenumitvarNode. - Se atribuie tipul
VARIABLE_DECLARATIONnoului nod.
- Se creează un obiect
-
Trecerea de Atomul Lexical 'var':
- Se trece la următorul atom lexical din vector prin apelul funcției
consume(). Acest pas marchează că am trecut de token-ul 'var'.
- Se trece la următorul atom lexical din vector prin apelul funcției
-
Verificarea Tipului Atomului Lexical Curent (
current().type):- Se verifică dacă atomul lexical curent este de tip
NAME(identificator).- Dacă este
NAME, se continuă cu analiza identificatorului prin apelul funcțieiparseIdentifier()și rezultatul este adăugat la lista de copii a noduluivarNode. - Se verifică apoi dacă următorul atom lexical este de tip
EQUALS(operator de atribuire).
- Dacă este
- Se verifică dacă atomul lexical curent este de tip
-
Verificarea Operatorului de Atribuire (
TokenType::EQUALS):- Dacă atomul lexical curent este de tip
EQUALS, atunci se considera ca in nodulVARIABLE_DECLARATIONse face si o operatie deatribuire, astfel se trece peste acest token (EQUALS) și se adaugă în lista de copii a noduluivarNoderezultatul apelului funcțieiparseExpression(). - Funcția
parseExpression()analizează și construiește un nod pentru expresii aritmetice sau logice asociate variabilei.
- Dacă atomul lexical curent este de tip
-
Tratarea Situațiilor Neașteptate:
- În caz contrar, dacă atomul lexical nu este de tip
NAME, se afișează o eroare prin intermediulstd::cerr, deoarece dupa un atom de tipVAR( declaratie de variabila ) intotdeauna se afla un atom de tipNAME( identifier ).
- În caz contrar, dacă atomul lexical nu este de tip
-
Returnarea Nodului Variabilei (
varNode):- Se returnează nodul
varNode, care conține informațiile despre declarația variabilei, inclusiv numele și, dacă există, expresia asociată.
- Se returnează nodul
ASTNode AST::parseDeclaration()
{
ASTNode varNode;
varNode.type = ASTNodeType::VARIABLE_DECLARATION;
consume(); // Trecem de atomul lexical 'var'
if (current().type == TokenType::NAME)
{
// Analizăm numele variabilei
varNode.children.push_back(parseIdentifier());
// Verificăm pentru specificatorul de atribuire
if (current().type == TokenType::EQUALS)
{
consume(); // Trecem de atomul lexical 'equals'
varNode.children.push_back(parseExpression());
}
}
else
{
std::cerr << "Eroare, atom lexical invalid!";
}
return varNode;
}Clasa Translator are rolul de a traduce un Arbore de Sintaxă Abstractă (AST) în cod sursă în limbajul de programare C++. Aceasta facilitează conversia structurilor sintactice ale unui program într-un format care poate fi înțeles și executat de un compilator C++. Principalele funcționalități ale clasei includ:
Funcția ast_to_cpp reprezintă inima clasei Translator, asigurând traducerea adecvată a unui nod din Arborele de Sintaxă Abstractă (AST) în cod sursă C++. Iată cum funcționează și cum este gestionată traducerea pentru diverse tipuri de noduri:
-
Structura Funcției:
- Funcția
ast_to_cppeste definită static în clasaTranslatorși primește două argumente:node, care reprezintă nodul din AST care trebuie tradus, șidepth, care indică nivelul de înglobare pentru formatarea corectă a codului.
- Funcția
-
Abordarea Recursivă:
- Funcția utilizează o abordare recursivă pentru a explora toate ramurile arborelui, traducând fiecare nod și gestionându-și copiii.
- Pentru fiecare tip de nod, funcția identifică tipul și aplică logica specifică pentru traducerea acelui tip.
-
Utilizarea Lambda pentru Adăugarea de Indentații:
- Funcția utilizează o lambda
add_indentationpentru a adăuga spații albe pentru formatarea corectă a codului în funcție de nivelul de înglobare (depth). - Aceasta furnizează un mod elegant de a introduce spații și linii goale pentru evidențierea structurii blocurilor de cod.
- Funcția utilizează o lambda
-
Instrucțiuni Switch Pentru Tipurile de Noduri:
- Funcția utilizează o instrucțiune
switchpentru a identifica tipul nodului (node.type) și a apela funcția corespunzătoare pentru traducerea acelui tip de nod. - Fiecare caz în
switchcorespunde unui tip specific de nod din AST.
- Funcția utilizează o instrucțiune
-
Traducerea Diferitelor Tipuri de Noduri:
- Funcția conține cazuri pentru diferite tipuri de noduri, cum ar fi
VARIABLE_DECLARATION,ASSIGNMENT,IF_STATEMENT,STRING_LITERAL,NUMBER_LITERAL, etc. - În cazul nodurilor care au copii (de exemplu, blocuri de cod, expresii aritmetice), funcția apelată recursiv pentru a traduce și acei copii.
- Funcția conține cazuri pentru diferite tipuri de noduri, cum ar fi
-
Gestionarea Profunzimii Blocurilor de Cod:
- Pentru instrucțiunile de control de flux precum
IF_STATEMENTșiWHILE_LOOP, funcția asigură o formatare corectă a blocurilor de cod, adăugând spații în funcție de nivelul de înglobare (depth).
- Pentru instrucțiunile de control de flux precum
-
Returnarea Codului C++ Generat:
- Funcția returnează un șir de caractere care reprezintă codul C++ generat pentru nodul dat din AST.
- La nivelul cel mai înalt, această funcție poate fi apelată pentru a traduce întregul program.
Iată un exemplu simplificat al modului în care funcția ast_to_cpp ar putea traduce un nod de tip ASSIGNMENT:
case ASTNodeType::ASSIGNMENT:
{
std::string left_operand = ast_to_cpp(node.children[0], depth);
std::string right_operand = ast_to_cpp(node.children[1], depth);
return add_indentation() + left_operand + " = " + right_operand + ";";
}În acest exemplu, funcția traduce o instrucțiune de atribuire, aplicând recursiv traducerea pentru stânga și dreapta operatorilor, iar rezultatul este o linie de cod C++ corect indentată.
Funcția main din programul C++ are rolul de a coordona și controla fluxul principal al aplicației. În esență, aceasta îndeplinește următoarele funcționalități la modul general:
-
Verificarea Argumentelor de Linie de Comandă:
- Se verifică dacă programul a fost apelat cu un număr suficient de argumente de linie de comandă (cel puțin un nume de fișier de intrare).
- Se afișează mesaje de utilizare în caz de parametri insuficienți.
-
Procesarea Flag-urilor:
- Se analizează argumentele de linie de comandă pentru a determina dacă trebuie să se afișeze și vectorul de atomi lexicali și/sau arborele de sintaxă abstractă.
- Flag-urile
--tokensși--astactivează afișarea corespunzătoare.
-
Deschiderea și Citirea Fișierului de Intrare:
- Se deschide fișierul specificat de pe linia de comandă și se citește conținutul acestuia într-un șir de caractere (
script).
- Se deschide fișierul specificat de pe linia de comandă și se citește conținutul acestuia într-un șir de caractere (
-
Instantierea Obiectelor și Parsarea:
- Se creează un obiect de tip
Lexerși se apelează funcțiaparsepentru a transforma șirul de caractere într-un vector de atomi lexicali (tokens). - Se creează un obiect de tip
ASTși se apelează funcțiaparsepentru a transforma vectorul de atomi lexicali într-un Arbore de Sintaxă Abstractă (AST).
- Se creează un obiect de tip
-
Afișarea Atomilor Lexicali și a Arborelui de Sintaxă Abstractă (opțional):
- Dacă flag-urile
--tokenssau--astsunt activate, se afișează corespunzător vectorul de atomi lexicali sau arborele de sintaxă abstractă.
- Dacă flag-urile
-
Traducerea și Afișarea Codului C++ Generat:
- Se apelează funcția
ast_to_cdin clasaTranslatorpentru a genera codul C++ corespunzător arborelui de sintaxă abstractă. - Codul rezultat este afișat în terminal.
- Se apelează funcția
Prin urmare, funcția main acționează ca un orchestrator care gestionează fluxul de execuție al programului, coordonând interacțiunea între diferitele componente ale acestuia (Lexer, AST, Translator) pentru a realiza analiza lexicală, sintactică și generarea de cod C++.