Deadline etapa 1: 21 nov 2024 23:55 Deadline etapa 2: 9 dec 2024 23:55 Deadline etapa 3: 12 ian 2025 23:55 Schelet etapa 1 {{:lfa:2023:lfa2024-skel-etapa1.zip|}} Schelet etapa 2 {{:lfa:2024:proiect:src.zip|}} Schelet etapa 3 {{:lfa:2024:proiect:skel-etapa3.zip|}} ===== Etapa 3 ===== **Etapa 3** a proiectul consta in implementarea unui lexer in python si, pe baza acestuia, implementarea unui parser elementar pentru expresii lambda. === Ce este un 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 prin care un lexer poate fi implementat. Abordarea conventionala (si cea pe care o recomandam) consta in urmatorii pasi: - fiecare regex este **convertit intr-un AFN**, retinand in acelasi timp token-ul pe care il descrie precum si pozitia in care acesta apare in secventa. - **se construieste un AFN unic**, introducand o stare initiala si epsilon-tranzitii de la aceasta catre toate starile initiale ale AFN-urilor de mai sus. Astfel, acest AFN va accepta oricare dintre tokenii descrisi in specificatie. Fiecare stare finala din AFN-ul unic 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 un regex. 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** folosind un AFD precum cel descris in sectiunea anterioara, 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 de lungime mai mare ca $math[i] nu poate exista. In acelasi timp, cel mai lung subsir este cel mai lung cuvant $math[w(0,j)] cu $math[j spec = [(TOKEN_LEXEMA_1, regex1), (TOKEN_LEXEMA_2, regex2), ...] unde primul element din fiecare tuplu este un nume dat unui token, iar al doilea element din tuplu 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_cuvant)'' 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_LEXEMA_1, lexema1), (TOKEN_LEXEMA_2, lexema2), ...]'', unde ''TOKEN_LEXEMA_N'' este numele token-ului asociat lexemei n, pe baza specificatiei. === Exemplu === Fie specificatia urmatoare: spec = [("TOKEN1", "abbc*"), ("TOKEN2", "ab+"), ("TOKEN3", "a*d")] 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 lexare === Erorile de lexare sunt in general cauzate o configuratie gresita / incompleta sau de un cuvant invalid. Informatiile care trebuie transmise in acest caz trebuie sa ajute programatorul sa isi dea seama unde un cod s-a intamplat eroarea si care este tipul erorii. Din acest motiv vom afisa linia si coloana unde analiza lexicala a esuat si tipul erorii. Eroarea este echivalenta cu starea curenta: ''SINK_STATE'' a lexerului fara a trece in prealabil printr-o stare finala. In acest caz vom afisa un mesaj de eroare in formatul No viable alternative at character ..., line ... In primul loc liber vom pune indexul caracterului unde s-a oprit analiza (am ajuns in ''SINK_STATE'') indexat de la 0, iar in an doilea spatiu liber vom pune linia unde s-a intamplat asta (indexata de la 0). Daca analiza a ajuns la finalul cuvantului fara a accepta in prealabil o lexema, iar lexerul nu a ajuns in sink state, insa nici intr-o stare finala, vom afisa un mesaj de eroare in formatul: No viable alternative at character EOF, line ... Ca un mic rezumat: prima eroare apare atunci cand caracterul la care am ajuns este invalid si nu avem cum sa acceptam, iar a doua apare atunci cand lexerul ar mai accepta, insa cuvantul este incomplet si nu mai are ce. === Parser === Pentru partea de parser, veti avea de procesat expresii lambda ce pot fi generatede urmatoarea gramatica: ::= | '(' (' '|'+'|'-'|'*'|'\') ')' | '\'. ::= [a-zA-Z0-9]+ === Exemplu === \x.(x * (x + 2)) -> Lambda (var "x") -> Mul (var "x") (Parant (Plus (var "x") (val 2))) Pentru moment, exista doar testele pentru lexer! Zilele acestea vor aparea si cateva teste pentru parser ===== 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. Un alt check preliminar care se va face pe fiecare DFA construit va fi unul care verifica integritatea lui d.p.d.v. structural (starea initiala si starile finale sunt incluse in multima de stari, nu are tranzitii definite pe un caracter dintr-o anume stare). ==== 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]] 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]] 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 . └── src ├── __init__.py ├── DFA.py ├── NFA.py ├── Regex.py ├── Lexer.py ├── Parser.py ... (alte surse pe care le folositi) ├── ID.txt ====== Proiect ====== Proiectul consta in implementarea unui lexer in python. ===== Ce este un 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. ==== Care este input-ul unui lexer? ==== Lexer-ul primeste initial o specificatie de forma: TOKEN1 : REGEX1; TOKEN2 : REGEX2; TOKEN3 : REGEX3; ... unde fiecare ''TOKENi'' este un nume dat unui token, iar ''REGEXi'' este un regex ce descrie lexemele ce pot fi clasificate ca acel token. Puteti imagina aceasta specificatie ca un //fisier de configurare//, care descrie modul in care va functiona lexerul pe diverse fisiere de text. Inputul efectiv al unui lexer este un text care va fi impartit in lexeme folosind expresii regulate. In cursurile viitoare veti afla mai multe detalii despre cum functioneaza si cum sunt implementate lexerele. ==== Care este output-ul unui lexer? ==== Lexer-ul are ca output o lista de forma : ''[(lexema1, TOKEN_LEXEMA_1), (lexema2, TOKEN_LEXEMA_2), ...]'', unde ''TOKEN_LEXEMA_N'' este numele token-ului asociat lexemei n, pe baza specificatiei. ===== Etapa 1 ===== Datorita dificultatii lucrului direct cu regex-uri pentru verificarea apartenentei unui cuvant in limbaj, lexerele reale trec prin cateva etape intermediare inainte de inceperea analizei textului. Aceste etape construiesc un DFA pe baza regex-ului. **Etapa 1** a proiectului consta in conversia NFA in DFA (folosind Algoritmul __Subset Construction__ prezentat la curs) Tema va fi implementata in Python, iar scheletul va ofera un pattern pentru implementarea functionalitatilor necesare. ====Structura scheletului==== In scheletul temei veti gasi 2 clase: **NFA** si **DFA**. In fiecare veti avea de implementate metodele necesare pentru verificarea comportamentului lor si pentru a realiza conversia ceruta de tema. === Clasa DFA === Un DFA va fi descris de urmatoarele campuri: S - alfabetul limbajului, reprezentat ca un set de string-uri K - starile automatului, reprezentat ca un set de __STATE__ q0 - starea initiala a automatului d - functia de tranzitie, reprezentata ca un dictionar cu cheie (stare, caracter_alfabet) si alta stare ca si valoare F - starile finale ale automatului Desi cea mai simpla modalitate de a ne referi la o stare este printr-un numar intreg, in anumite componente ale proiectului (si de la aceasta etapa, dar si de la etape viitoare) va fi mult mai convenabil sa lucram cu alte tipuri de etichete pentru stari (de exemplu, seturi de intregi sau tupluri). De aceea, parametrul ''STATE'' va permite sa utilizati ce tip doriti ca si stare a automatului, puteti nota starile automatului atat cu intregi ''0,1,2,3,...'' cat si cu siruri de caractere ''s0,q1,sink,...'' sau alte tipuri de date (''frozenset'') - In aceasta clasa veti avea de implementat **obligatoriu** functia ''accept'', functie care primeste un cuvant si ruland DFA-ul pe acel cuvant va intoarce ''True'' daca cuvantul este acceptat, iar ''False'' in caz contrar. - Functia ''remap_states'' **nu este** obligatoriu de implementat, din moment ce nu este apelata de checker, insa este recomandata din moment ce va va usura implementarea algoritmului subset construction. Ea are ca scop **transformarea** setului de stari, de la un tip (spre exemplu //string//) la un altul (spre exemplu, //integer//). Astfel de transformari vor fi necesare, in special in etapele ulterioare ale proiectului. Spre exemplu, daca am avea automatul de mai jos: {{:lfa:2023:remap_before.png?400|}} /* > (0) -a,b-> (1) ----a----> ((2)) \-b-> (3) <-a,b-/ / \ \-a,b-/ */ Am putea aplica functia ''x -> 'q' + str(x+2)'', care ar creea un DFA cu urmatoarele stari: {{:lfa:2023:remap_after.png?400|}} /* > (q2) -a,b-> (q3) ----a----> ((q4)) \-b-> (q5) <-a,b-/ / \ \-a,b-/ */ ===Clasa NFA=== Clasa functioneaza in aceeasi maniera cu cea a DFA-ului, cu o singura diferenta: * Spre deosebire de reprezentarea de la curs, unde $math[\Delta] reprezenta o relatie peste $math[K \times \Sigma \times K], in Etapa 1, ''d'' va fi tot o functie, (codificata printr-un dictionar), care va asocia unei perechi (stare, caracter_alfabet), un **set de stari** succesor (in loc de o stare unica, asa cum se intampla intr-un DFA). Alte observatii: - Functia ''epsilon_closure'' primeste o stare a automatului si intoarce un set de stari, care reprezinta starile la care se poate ajunge doar prin epsilon-tranzitii de la starea initiala (fara a consuma nici-un caracter) - Functia ''subset_construction'' va intoarce un DFA, construit din NFA-ul curent prin algoritmul __subset construction__. DFA-ul intors va avea ca tip al starilor ''frozenset[STATE]'' (starile ''0'' si ''1'' dintr-un NFA vor ajunge o multime de stari ''{0,1}'' dintr-un DFA). Folosim ''frozenset'' in loc de ''set'', pentru ca acesta din urma **nu este imutabil** (seturile pot fi modificate prin efecte laterale). Avem nevoie de un obiect **imutabil** pentru a putea calcula un hash (mereu acelasi), si implicit pentru a putea folosi astfel de obiecte drept chei intr-un dictionar (lucru imposibil daca obiectul-cheie este mutabil). - Functia ''remap_states'' care are acelasi format si scop ca functia de la DFA-uri Functiile ''epsilon_closure'' si ''subset_construction'' **sunt obligatoriu** de implementat, iar functia ''remap_states'' **nu** este. ===== Etapa 2 ===== **Etapa 2** a proiectului consta in parsarea unei expresii regulate precum si in conversia Regex - NFA (folosind Algoritmul Thompson prezentat la curs) ====Structura scheletului==== In scheletul temei veti gasi pe langa cele 2 clase de la etapele anterioare (**NFA** si **DFA**) inca o clasa, **Regex**, si o metoda ''parse_regex''. === Forma standard a expresiilor regulate === Forma standard a regex-urilor poate fi descrisa in forma [[https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form | BNF]] astfel: ::= | '|' | '*' | '+' | '?' | '(' ')' | "[A-Z]" | "[a-z]" | "[0-9]" | "eps" | In descrierea de mai sus, elementele dintre parantezele angulare <> sunt **non-terminali** care trebuie generati, caracterele sunt intotdeauna plasate intre ghilimele simple, iar sirurile de caractere - intre ghilimele duble. '''' se refera la orice caracter care nu face parte din caracterele de //control// (precum ''*'' sau ''|''), sau la orice sir de lungime doi de forma ''\c'', unde ''c'' poate fi orice caracter inclusiv de control (mai exact - caracter escapat). "eps" reprezinta caracterul $math[\epsilon]. ==== Preprocesarea Regex-urilor ==== In descrierea de mai sus, pe langa caracterele alfa-numerice si operatiile de baza star, concat si union, veti gasi si: - doua operatii noi: - plus ''+'' - expresia asupra careia este aplicat apare de 1 data sau mai multe ori. - semnul intrebarii ''?'' - expresia asupra careia este aplicat apare o data sau niciodata. - 3 syntactic sugars: - ''[a-z]'' - orice caracter litera mica din alfabetul englez - ''[A-Z]'' - orice caracter litera mare din alfabetul englez - ''[0-9]'' - orice cifra Daca un regex contine spatii albe, acestea sunt ignorate. Pentru a nu fi ignorate, acestea vor fi precedate de un backslash "\ ". Similar, pentru a nu incurca caracterele '*', '+', ')', '(', '|', '?' care reprezinta si operatori din regex-uri, cand fac propriu zis parte din regex, vor fi precedate de backslash. ===Clasa Regex=== In aceasta clasa veti avea de implementat metoda ''thompson'', metoda care primeste un obiect de tip regex si intoarce un NFA (cu starile de tipul ''int'' ca si conventie). Regexul primit ca input va avea forma prezentata mai sus. Concatenarea nu va fi reprezentata printr-un caracter anume, vom considera ca constructiile de forma ''ab'' se traduc automat in "caracterul a concatenat cu caracterul b". Concatenarea oprindu-se astfel la intalnirea **unei paranteze** sau **a unei reuniuni**. Spre exemplu: - ''ab|c'' se traduce in ''(ab)|c'' - ''abd*'' se traduce in ''ab(d)*'' - ''ab+'' se traduce in ''a(b)+'' === Hint de implementare === Pentru a va usura lucrul cu expresiile regulate, puteti creea mai multe clase care extind clasa regex (ex ''Character'', ''Star'', ''Union'', ''Concat'', etc...) iar fiecare clasa va avea propria implementare a metodei ''thompson''.