Edit this page Backlinks This page is read only. You can view the source, but not change it. Ask your administrator if you think this is wrong. Deadline etapa 1: 26 nov 2025 23:55 Deadline etapa 2: 1 ian 2026 23:55 Link etapa 1 [[lfa:2025:proiect:etapa1| Etapa 1]] ====== Etapa 2 ====== **Etapa 2** a proiectul consta in implementarea unui Lexer in Python si, pe baza acestuia, implementarea unui Parser elementar pentru expresii lambda. Pentru rezolvarea etapei 2, aveti nevoie de solutia de la etapa 1. ==== Lexer ==== Un lexer este un program care imparte un sir de caractere in subsiruri numite //lexeme//, fiecare dintre acestea fiind clasificat ca un //token//, pe baza unei specificatii. Specificatia contine o secventa de perechi $math[(token_i, regex_i)] in care fiecare token este descris printr-o expresie regulata. Ordinea perechilor in secventa este importanta, iar acest aspect va fi discutat ulterior. Exista mai multe modalitati pentru implementarea unui lexer. Abordarea conventionala (si cea pe care o recomandam) este urmatoarea: - fiecare regex din specificatie este **convertit intr-un AFN**, retinand in acelasi timp token-ul pe care il descrie precum si pozitia in care acesta apare in specificatie. - toate AFN-urile provenite din regex-uri sunt combinate intr-un **un AFN unic**, astfel: creem o noua stare initiala si epsilon-tranzitii de la aceasta catre toate starile initiale ale AFN-urilor mentionate anterior. Fiecare stare finala din AFN-ul astfel obtinut va indica token-ul corespunzator gasit. - AFN-ul este convertit la un AFD (care optional poate fi minimizat). In acest automat: - cand vizitam un grup de stari ce contine (AFN-)stari finale, inseamna ca unul sau mai multe token-uri corespunzatoare au fost identificate. - cand vizitam un sink-state (daca acesta exista), inseamna ca subsirul curent nu este descris de nici un token. Pentru ca analiza lexicala sa poata functiona corect, AFD-ul descris anterior este folosit pentru a identifica, la fiecare pas, **cel mai lung subsir** din sirul analizat, care e generat de unul din regex-urile din specificatie. In situatia in care un astfel de subsir e generat de doua sau mai multe regex-uri, va fi raportat primul token aferent, in raport cu ordinea din specificatie. Pentru a identifica **cel mai lung subsir**, trebuie sa observam faptul ca, daca ne aflam la pozitia $math[i] in sirul $math[w] de la input si: - ne aflam intr-un grup de stari ce contine o (AFN-)stare finala, atunci $math[w(0,i)] **nu este in mod necesar** cel mai lung subsir - daca ne aflam in sink-state-ul AFD-ului inseamna ca un subsir generat de un regex, de lungime mai mare ca $math[i] nu poate exista. - cel mai lung subsir este intotdeauna cel mai lung cuvant $math[w(0,j)] cu $math[j\leq i], daca acesta a fost acceptat anterior, in cazul in care un astfel de cuvant exista. - daca in AFD nu exista un sink state, atunci analiza lexicala trebuie sa continue pana la epuizarea inputului, pentru a decide asupra celui mai lung subsir. Odata ce subsirul cel mai lung a fost identificat: - AFD-ul va fi //resetat// - adus in starea initiala pentru a relua analiza lexicala. - analiza lexicala va continua de la pozitia unde subsirul cel mai lung s-a terminat. === Clasa Lexer === Clasa lexer are un constructor care primeste ca parametru o **specificatie** care are urmatoarea structura: <code python> spec = [(TOKEN_1, regex1), (TOKEN_2, regex2), ...] </code> unde primul element din fiecare tuplu este un string ce reprezinta token-ul, iar al doilea element este un regex ce descrie acel token. Puteti imagina aceasta specificatie ca un //fisier de configurare// care descrie modul in care va functiona lexerul pe diverse fisiere de text. In plus, clasa lexer contine functia ''lex'' care va primi un cuvant ''str'' ca input si va intoarce rezultatul analizei lexicale sub forma ''list[tuple[str, str]]''. Metoda va intoarce o lista de tupluri ''(token, lexem)'' in cazul in care analiza reuseste. In caz de eroare, se va intoarce o lista cu un singur element de forma ''("", "No viable alternative at character _, line _")'' (//Mai multe despre cazurile in care un lexer poate esua mai jos//). Astfel, metoda are ca output o lista de forma : ''[(TOKEN_1, lexema1), (TOKEN_2, lexema2), ...]''. === Exemplu === Fie specificatia urmatoare: <code> spec = [("TOKEN1", "abbc*"), ("TOKEN2", "ab+"), ("TOKEN3", "a*d")] </code> si input-ul ''abbd''. Analiza lexicala se va opri la caracterul ''d'' (AFD-ul descris anterior va ajunge pe acest caracter in sink state). Subsirul ''abb'' este cel mai lung care satisface atat ''TOKEN1'' cat si ''TOKEN2'', iar ''TOKEN1'' va fi raportat, intrucat il preceda pe ''TOKEN2'' In specificatie. Ulterior, lexerul va devansa cu un caracter pozitia curenta in input, si va identifica subsirul ''d'' ca fiind ''TOKEN3''. Pentru lamuriri ulterioare si mai multe exemple ce includ cel mai lung subsir, revizitati cursul aferent lexerelor. === Erori de analiza lexicala === Erorile de analiza lexicala sunt in general produse o specificatie gresita / incompleta sau de un cuvant invalid. Informatiile care trebuie transmise in acest caz trebuie sa ajute programatorul sa isi dea seama unde a avut loc eroarea si care este tipul erorii. Din acest motiv vom afisa linia si coloana unde analiza lexicala a esuat si tipul erorii. Semnalam o eroare cand lexerul ajunge in sink state fara sa fie trecut in prealabil printr-o stare finala. In acest caz vom afisa: No viable alternative at character N, line X Unde N este pozitia in sir a caracterului unde s-a oprit analiza (indexat de la 0), iar X este linia aferenta (indexata de la 0). Daca analiza a ajuns la finalul cuvantului fara a accepta in prealabil, iar lexerul nu a ajuns in sink state, insa nici intr-o stare finala, vom afisa: No viable alternative at character EOF, line X ==== Parser ==== Un parser foloseste output-ul produs de lexer pentru etapa de analiza sintactica a textului. Construim un parser pe baza unei gramatici care contine reguli ce descriu sintaxa valida a inputului. O astfel de gramatica va folosi lexemele generate de analiza lexicala in rolul de terminali. === Sintaxa pentru gramatici === In implementarea noastra, sintaxa folosita pentru descrierea gramaticilor in format text va fi urmatoarea: <code> s: a b s: b a a: A b: B </code> unde a si b sunt neterminali si A si B sunt terminali. Preferam sa notam neterminalii cu litere mici si terminalii cu litere mari deoarece terminalii nostri vor fi tokenii proveniti de la analiza lexicala. De asemenea, putem scrie simplificat primele 2 reguli astfel, folosind o alternativa: <code> s: a b|b a </code> E important ca inainte si dupa simbolul pentru alternativa sa nu existe spatii, pentru citirea corecta a fisierului. In mod automat simbolul care introduce prima regula din gramatica este considerat simbolul de start, indiferent de denumirea lui. Gramaticile folosite de noi vor fi intotdeauna in Forma Normala Chomsky, pentru a putea aplica algoritmul care valideaza apartenenta unui cuvant la limbajul generat de gramatica. O gramatica este in FNC daca are doar reguli de tipul a: b c (un neterminal produce doi neterminali) sau a: A (un neterminal produce un singur terminal). === Verificarea acceptarii unui cuvant de catre gramatica === Pentru a verifica daca un cuvant apartine limbajului descris de o gramatica in FNC folosim algoritmul CYK. Algoritmul CYK poate fi consultat in materialele de la curs. === Arbore de parsare === In plus fata de algoritmul CYK implementat la curs, care ne spune daca un text este acceptat de gramatica, ne dorim sa avem si sirul de reguli prin care am ajuns la acceptare, adica sa obtinem la final un arbore de parsare pentru textul nostru. In scheletul temei aveti implementata o clasa numita ParseTree, care reprezinta arborele de parsare. Arborele de parsare va fi afisat ca un arbore in care copiii au o indentare cu 2 spatii mai la dreapta decat parintele. Pentru regulile intermediare generate de normalizarea gramaticii (care trebuie sa inceapa cu **int_**) nu se va afisa numele regulii, ci direct copiii. Pentru regulile simple, de tipul neterminal produce un terminal (a: TOKEN) se va afisa doar categoria lexicala si lexemul corespunzator, fara numele regulii. Aceste lucruri sunt deja implementate in metoda str() a lui ParseTree. Pentru a obtine un arbore de parsare ca rezultat, in implementarea algoritmului CYK va recomandam sa folositi o matrice de dictionare in care pentru fiecare neterminal sa aveti ca valoare un nod de ParseTree cu informatiile despre derivarile bottom-up care au dus la obtinerea acelui neterminal. La final, in caz de acceptare, arborele de parsare afisat va fi nodul de arbore aferent simbolului de start. === Cerinta: === Implementati un parser general, care primeste o gramatica in FNC si returneaza un arbore de parsare. 1. In clasa Grammar completati metoda "cykParse", care primeste output-ul unui lexer (tupluri de (token, lexema)) si intoarce arborele de parsare. 2. Scrieti o gramatica in FNC in fisierul "**grammar_lambda.txt**", pornind de la gramatica pentru expresii lambda din fisierul "**parser_grammar.txt**". Aplicati voi transformarile prezentate la curs pentru a obtine FNC. Toti neterminalii creati in acest proces trebuie sa inceapa cu "int_", pentru a fi distinsi de ceilalti neterminali, astfel incat sa ii omitem cand afisam arborele de parsare. Astfel outputul este mult mai usor de urmarit. 3. Completati specificatia pentru Lexerul de expresii lambda in fisierul "**lexer_spec.json**", adaugand regex-ul potrivit pentru fiecare Token. 4. In clasa Parser completati metoda "parse", pentru a scrie un parser general care citeste o gramatica in FNC din fisierul primit ca parametru la initializare si returneaza arborele de parsare. === Exemplu de parsare === Pentru textul "int x = 1 + 2" analiza lexicala a produs tokenii [(TYPE, "int"), (ID, "x"), (EQUAL, "="), (NUMBER, "3"), (PLUS, "+"), (NUMBER, "2")] (tokenii de SPACE au fost ignorati). Parserul e configurat cu urmatoarea gramatica (care in exemplu nu e in FNC, dar inainte de a fi interpretata de Parser a fost normalizata): <code> assign: TYPE ID EQUAL sum sum: NUMBER PLUS NUMBER </code> Arborele de parsare obtinut va fi: <code> assign (TYPE: int) (ID: x) (EQUAL: =) sum (NUMBER: 1) (PLUS: +) (NUMBER: 2) </code> ===== Testare ===== Verificarea corectitudinii implementarii voastre se va face automat, printr-o serie de teste unitare, teste care vor verifica comportamentul fiecarei functii obligatorii de implementat si ii va testa output-ul pe o diversitate de input-uri. ==== Python ==== Versiunea de python pe care o vom folosi pentru aceasta tema este ''python3.12''. Un ghid de instalare a acestei versiuni poate fi gasita [[https://aruljohn.com/blog/install-python/|aici]] <note important> Este recomandat sa parcurgeti [[lfa:2023:lab_python_extras|documentul extra]] pentru descrierea unor feature-uri folosite in schelet si a unora utile in implementarea proiectului, mai ales topic-urile: * [[lfa:2023:lab_python_extras#dictionaries_sets_and_hashable_objects | hashing]] * [[lfa:2023:lab_python_extras#python_312_generics | genericitate in python 3.12]] * [[lfa:2023:lab_python_extras#dataclasses | decoratorul dataclass]] </note> Pentru rularea testelor folositi comanda ''python3.12 -m unittest''. Aceasta comanda va detecta automat testele definite in folder-ul ''test'' si le va rula pe rand, afisand la final testele care au esuat, daca exista. ==== Structura arhivei ==== Veti incarca in assignment-ul de pe moodle o arhiva ''zip'' care va avea la baza folderul ''src'' din schelet si fisierul ''ID.txt'' ce contine user@stud.acs.pub.ro pe prima linie <code> . ├── grammar_lambda.txt ├── lexer_spec.json ├── src │ ├── __init__.py │ ├── DFA.py │ ├── Grammar.py │ ├── Lexer.py │ ├── NFA.py │ ├── Parser.py │ ├── ParseTree.py | ... (alte surse pe care le folositi) ├── ID.txt </code>