Differences
This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
lfa2021:proiect [2021/08/31 09:44] pdmatei |
lfa2021:proiect [2021/11/24 14:21] (current) stefan.stancu [Structura punctajului] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Etapa 4 - Parsare Regex si limbaj ====== | + | ===== Proiect LFA - Lexer in Python sau Haskell ===== |
- | Serializare lexer (ca sa ne legam direct cu etapa 1). | + | ==== Structura punctajului ==== |
- | Apoi parsare. Doua programe. | + | * Proiectul valoreaza **3p**, care se impart egal intre etape. |
+ | * O etapa se considera **trecuta**, daca, concomitent: (i) **a fost trimisa la deadline** si (ii) $math[pr=]70% din teste au trecut. | ||
+ | * Daca o etapa **nu** a fost **trecuta**, ea poate fi trimisa sau retrimisa oricand, pentru $math[pmin=]60% din punctaj (adica 60% din valoarea testelor care trec). | ||
+ | * Orice etapa **trecuta** poate fi retrimisa oricand, fara depunctare, pentru a obtine puncte in plus pentru teste aditionale care trec. | ||
+ | * Exemple: | ||
+ | * Studentul MP nu a **trecut** etapele 1 si 2 (nu a trimis etapa 1 la timp, si doar jumatate din teste au trecut la etapa 2), insa la etapa 3 a trimis tot proiectul 100% functional (toate testele trec). Punctajul lui este: 0.6p (Etapa 1) + 0.6p (Etapa 2) + 1p (Etapa 3) = 2.2p. | ||
+ | * Studentul DM a **trecut** etapa 1 (cu 70% din teste), la fel si etapa 2 (tot cu 70% din teste). In loc sa trimita etapa 3, a lucrat la etapa 1 si a reusit sa faca toate testele sa treaca la aceasta. Punctajul lui este 1p (Etapa 1) + 0.7p (Etapa 2) = 1.7p. | ||
+ | * Studentul XZ nu a **trecut** etapa 1 (a trimis la timp insa doar 50% din teste trec) si nu a trecut nici etapa 2 (din nou, a trimis la timp, insa doar 60% din teste trec). Etapa 3 nu a fost trimisa. Punctajul lui este 0.3p la etapa 1 (1p * 0.5teste * 0.6penalty) si 0.36p la etapa 2 (1p * 0.6teste * 0.6penalty)) | ||
+ | * **Ultima etapa va avea un deadline hard final** (punctajul obtinut pe aceasta este exclusiv cel de pe teste). Nu se mai pot trimite submisii (la nici o etapa), dupa expirarea deadline-ului acesta. | ||
- | ==== Limbajul 1: Imperative ==== | ||
- | <code> | ||
- | <prog> ::= <variable> '=' <expr>; | | ||
- | begin <instruction_list> end | | ||
- | while (<expr>) do <prog> od | | ||
- | if (<expr>) then <prog> else <prog> fi | ||
- | |||
- | <instruction_list> ::= [<prog>] | ||
- | |||
- | <expr> ::= <expr> + <expr> | <expr> > <expr> | <expr> == <expr> | <variable> | <integer> | ||
- | |||
- | </code> | ||
- | |||
- | ==== Limbajul 2: Lisp ==== | ||
- | |||
- | ==== Limbajul 3: Html ??? ==== | ||
- | |||
- | |||
- | ===== Metodologia de testare: ===== | ||
- | Combinam expresii din fisierul de referinta in specificatii. Mai sunt necesare detalii de clarificat. | ||
- | |||
- | ====== Etapa 3 - Conversie AFN AFD ====== | ||
- | |||
- | ===== Metodologia de testare: ===== | ||
- | |||
- | Asemanator cu etapa 2. Folosim acelasi fisier de input, insa generam, in locul expresiilor PRENEX, nfa-uri, din lista de referinta. | ||
- | |||
- | ====== Etapa 2 - Conversie Regex AS la AFN ====== | ||
- | |||
- | Obiectivul etapei 2 este conversia unei expresii regulate la un Automat Finit Nedeterminist. Aceasta transformare este un prim pas in conversia unei **specificatii** (expresia regulata) intr-o **implementare** (AFN). | ||
- | |||
- | ===== Descrierea inputului ===== | ||
- | * un prim pas in aceasta etapa este parsarea expresiilor regulate, si construirea unui arbore pentru acestea. Spre exemplu, expresia: ''ab|c*'' are asociat arborele: ''UNION(CONCAT(a,b),STAR%%(%%c))''. Datorita regulilor de precenta ale operatorilor, construirea acestui arbore este mai complicata, iar aceast task va fi amanat pentru etapa 4. Pentru a simplifica problema parsarii, expresiile vor fi prezentate **in forma prenex**. | ||
- | * In **forma prenex**, **//operatorul preceda intotdeauna//** operanzii. Prezentate astfel, expresiile pot fi parsate mult mai usor. Spre exemplu, expresia de mai sus in forma **prenex** este ''UNION CONCAT a b STAR c''. Observati similaritatea cu reprezentarea de arbore. | ||
- | * Un test consta intr-un fisier ''.in'' ce contine: | ||
- | * **pe prima linie**, o expresie regulata in forma prenex | ||
- | * **in continuare**, cate un cuvant pe o linie | ||
- | * Exemplu: | ||
- | <code> | ||
- | UNION CONCAT a b STAR c | ||
- | abc | ||
- | abcccc | ||
- | ccc | ||
- | ab | ||
- | </code> | ||
- | |||
- | ===== Cerinta si descrierea outputului ===== | ||
- | |||
- | Implementarea voastra va primi un fisier de test ''<testxy.in>'', va construi **AFN-ul asociat** expresiei regulate, apoi va verifica daca acesta accepta cuvintele din fisierul de test. Implementarea va scrie la output un fisier ''<testxy.out>'' ce va contine, pe cate o linie ''ACCEPT'' sau ''REJECT'', pentru fiecare din cuvintele de la input. Exemplu de fisier de output pentru input-ul anterior: | ||
- | |||
- | <code> | ||
- | REJECT | ||
- | REJECT | ||
- | ACCEPT | ||
- | ACCEPT | ||
- | </code> | ||
- | |||
- | **Atentie**: Implementarea va fi punctata doar in masura in care respecta cerinta (se construieste un AFN folosind algoritmul prezentat la curs). | ||
- | |||
- | ===== Identitatea starilor ===== | ||
- | |||
- | Implementarea algoritmului lui Thomson este complicata de faptul ca starile sunt reprezentate ca intregi. Spre exemplu, AFN-urile pentru expresiile ''0'' si ''1'' vor avea fiecare cate doua stari, cel mai probabil numerotate de la 0 la 1. Pentru a construi AFN-ul pentru ''0|1'', trebuie sa **renumerotam** starile tinand cont de: | ||
- | * faptul ca trebuie sa creem o noua stare initiala, si una finala | ||
- | * starile AFN-ului pentru ''0'' trebuie sa fie **disjuncte** fata de cele ale lui ''1'' | ||
- | * logica AFN-urilor (ce cuvinte accepta) nu trebuie sa influentata de numerotare | ||
- | * renumerotarea trebuie sa fie **suficient de generala** (vom mai avea nevoie de ea la etapa 3), astfel incat sa se comporte ca un **map** (o aplicatie de //functie// peste stari). In aceasta etapa, //functia// va fi ''+x''. | ||
- | |||
- | ===== Parsarea formei Prenex ===== | ||
- | |||
- | Pentru a parsa forma Prenex, avem nevoie de o stiva care sa retina parti ale expresiei / operatii parsate deja. Vom interactiona in doua feluri cu stiva: | ||
- | * **reducerea expresiilor** (sau cooling). | ||
- | * Exemplul 1: daca pe stiva avem: ''0 | Star(?) | ... '', atunci vom inlocui cele doua expresii cu : '' Star(0) | ...'' | ||
- | * Exemplul 2: daca pe stiva avem: ''Star(0) | Concat(?,?) | ... '', rezultatul reducerii va fi: ''Concat(Star(0),?) | ...'' | ||
- | * ** adaugarea expresiilor**: vom citi operatorul sau operandul curent, si vom adauga elementele corespunzatoare pe stiva. | ||
- | |||
- | Implementarea voastra trebuie sa combine in mod eficient **adaugarea** cu **reducerea**. | ||
- | |||
- | ===== Developlement si testare ===== | ||
- | |||
- | Pentru a testa AFN-urile generate de voi, aveti nevoie de o modalitate de afisare a acestora in text. Va sugeram sa ii adaugati si modalitate grafica. Atasam un script Python3 care poate fi folosit pentru a desena AFN-uri: | ||
- | * script-ul primeste in linia de comanda un fisier CSV ce contine un AFN, si deseneaza AFN-ul respectiv. | ||
- | * va fi necesar sa instalati anumite dependinte pentru a rula scriptul (e.g. networkx, numpy). | ||
- | * trebuie sa scrieti o procedura de afisare a unui AFN sub forma de CSV. Atasam un exemplu (AFN-ul generat pentru expresia ''00*''): | ||
- | <code> | ||
- | from,char,to | ||
- | 0,0,1 | ||
- | 1,ε,2 | ||
- | 2,ε,3 | ||
- | 2,ε,f5 | ||
- | 3,0,4 | ||
- | 4,ε,3 | ||
- | 4,ε,f5 | ||
- | </code> | ||
- | * prima linie este obligatorie si aceeasi pt orice AFN | ||
- | * tranzitiile sunt codificate pe linii, sub forma: ''stare initiala, caracter, stare finala'' | ||
- | * pentru legibilitate, starile finale au fost precedate de simbolul ''f'' | ||
- | * [TODO: link catre script pe github-ul LFA] | ||
- | |||
- | |||
- | |||
- | ===== Sugestii de implementare pentru Python ===== | ||
- | |||
- | * Folositi **clase** pentru reprezentarea interna a unei expresii regulate. Vom refolosi aceasta reprezentare in etapa 4. | ||
- | * Alegeti o implementare pentru AFN-uri care sa permita **refolosirea** (extinderea) ei pentru situatii in care **starile** au alta structura. (multimi de intregi in loc de intregi). | ||
- | * In procesul de parsare, instantiati cu ''None'' expresii a caror parsare este in curs de desfasurare. Ele vor fi modificate ulterior. | ||
- | |||
- | ===== Sugestii de implementare pentru Haskell ===== | ||
- | |||
- | * In tipul vostru de date care va retine expresii, adaugati constructori pentru expresii a caror parsare este in curs de desfasurare. Acestia vor fi folositi doar in timpul parsarii formei prenex. Spre exemplu, pe langa ''Concat :: Expr -> Expr -> Expr'', definiti ''LConcat :: Expr -> Expr''. Acesta va codifica faptul ca am citit doar partea din **stanga** a unei concatenari. Asemanator, ''LRConcat :: Expr'' va codifica faptul ca am citit o concatenare si urmeaza sa citim operanzii acesteia. | ||
- | * Interactiunea cu stiva se face cel mai natural folosind **pattern matching** (si este mult mai simplu de implementat in Haskell). | ||
- | * Pentru codificarea de NFA-uri: | ||
- | * Adaugati clasei ''FA'', definita la etapa anterioara, metoda ''relabel'' (si implementati-o): | ||
- | |||
- | <code Haskell> | ||
- | class FA t where | ||
- | fromList :: State s => Set Char -> s -> [(s,Char,s)] -> [s] -> t s | ||
- | states :: (State s) => t s -> [s] | ||
- | relabel :: (State s, State s') => (s -> s') -> t s -> t s' | ||
- | size :: (State s) => t s -> Integer | ||
- | </code> | ||
- | * Bonus: de ce nu am folosit ''fmap'', in locul functiei ''relabel''? | ||
- | | ||
- | * Definiti tipul ''Nfa a'' si inrolati-l in clasa anterioara. O definitie (mai multe sunt posibile) este urmatoarea: | ||
- | <code Haskell> | ||
- | type NDelta a = Map (a,Char) (Set a) | ||
- | data Nfa a = Nfa {nsigma :: Set Char, ninitial :: a, ndelta :: (NDelta a), nfin :: [a]} | ||
- | </code> | ||
- | |||
- | |||
- | |||
- | |||
- | |||
- | ===== Metodologia de testare: ===== | ||
- | * construim un fisier mare cu expresii regulate **de mana**, peste diverse alfabete, care sa acopere cat mai multe din toate situatiile posibile, impreuna cu cuvinte acceptate sau nu. Acest fisier va trebui discutat eventual cu fiecare dintre noi. Exemplu: | ||
- | <code> | ||
- | 0 | ||
- | ACCEPT 0 | ||
- | REJECT 01 | ||
- | REJECT 10 | ||
- | | ||
- | 00* | ||
- | ACCEPT 0000000000 | ||
- | REJECT 1 | ||
- | REJECT ε | ||
- | </code> | ||
- | * parsam expresiile cu implementarea noastra, generam forma prenex, si creem **mai multe perechi de fisiere separate, input-output** - in fiecare fisier input se va gasi o expresie si cuvintele de test. Ficare fisier de output va marca rezultatul asteptat. Exemplu: | ||
- | * Test0.in | ||
- | <code> | ||
- | 0 | ||
- | 0 | ||
- | 01 | ||
- | 10 | ||
- | </code> | ||
- | * Test0.out | ||
- | <code> | ||
- | ACCEPT | ||
- | REJECT | ||
- | REJECT | ||
- | </code> | ||
- | * Test1.in | ||
- | <code> | ||
- | CONCAT 0 STAR 0 | ||
- | 0000000000 | ||
- | 1 | ||
- | ε | ||
- | </code> | ||
- | * Test1.out | ||
- | | ||
- | <code> | ||
- | ACCEPT | ||
- | REJECT | ||
- | REJECT | ||
- | </code> | ||
- | |||
- | * Vom verifica, fiecare, ca outputul este corect, folosind implementarile noastre. | ||
- | |||
- | ====== Etapa 1 - Your own personal Lexer ====== | ||
- | |||
- | Etapa 1 consta in implementarea unui lexer simplu (in Python sau Haskell). | ||
- | |||
- | ==== Ce este un lexer? ==== | ||
- | |||
- | Un lexer este un program care imparte un sir de caractere in subsiruri numite **lexeme**, fiecare fiind clasificat ca un **token**. Tabelul de mai jos ilustreaza cateva perechi de tokens si lexeme: | ||
- | ^ Token ^ Lexeme ^ | ||
- | | **variabla** | x | | ||
- | | **variabla** | var | | ||
- | | **valoare** | 23 | | ||
- | | **plus** | + | | ||
- | | **eq** | = | | ||
- | |||
- | Spre exemplu, sirul ''x=12+y'' va fi transformat de lexerul implementat de voi in: ''(variabila,"x"),(eq,"="),(valoare,"12"),(plus,"+"),(variabila,"y")''. | ||
- | |||
- | ==== Ce primeste la input un lexer? ==== | ||
- | |||
- | In realitate, un lexer primeste la input o specificatie de tokens, si produce un program de scanare a inputului care este integrat, intr-o forma sau alta, cu un parser. In aplicatia noastra, lexer-ul va primi doua fisiere: | ||
- | - un fisier cu specificatia, descris mai jos. | ||
- | - un fisier cu un input (cuvantul) care va fi scanat. | ||
- | |||
- | === Specificatia ==== | ||
- | |||
- | In aceasta etapa, inputul unui lexer va fi conceptual foarte simplu, anume o lista: ''dfa1, dfa2, ... dfan'' de automate finite deterministe, fiecare codificand un anume token. Mai exact, inputul are urmatoarea structura: | ||
- | <code> | ||
- | <alfabet> | ||
- | <dfa1> | ||
- | |||
- | <dfa2> | ||
- | |||
- | <dfa3> | ||
- | |||
- | ... | ||
- | |||
- | <dfan> | ||
- | |||
- | |||
- | </code> | ||
- | |||
- | unde: | ||
- | * ''<alfabet>'' este un sir ce codifica alfabetul pentru **toate** AFD-urile | ||
- | * **Atentie: un alfabet valid poate contine orice simbol alfanumeric, prin urmare inclusiv SPATII ALBE**, care vor fi necesare pt implementarea etapei 4. | ||
- | * ''<dfai>'' este codificarea unui token impreuna cu AFD-ul asociat lui | ||
- | * codificarea fiecarui AFD se termina printr-o linie goala. | ||
- | |||
- | Codificarea unui AFD are urmatoarea structura: | ||
- | <code> | ||
- | <token> | ||
- | <stare_initiala> | ||
- | <tranzitie1> | ||
- | <tranzitie2> | ||
- | ... | ||
- | <tranzitie_n> | ||
- | <stari_finale> | ||
- | </code> | ||
- | unde: | ||
- | * ''<token>'' este **o linie** ce contine numele token-ului descris de AFD, deobicei scris cu litere mari (spre exemplu: ''VARIABILA'') | ||
- | * ''<stare_initiala>'' este **o linie** ce contine un intreg ce desemneaza starea initiala a AFD-ului (deobicei 0) | ||
- | * ''<stari_finale>'' este **o linie** ce contine o secventa de intregi separati printr-un spatiu alb ce desemneaza starile finale (spre exemplu: ''2 3'') | ||
- | * ''<tranzitie_i>'' este **o linie** condifica o tranzitie a AFD-ului, si are urmatoarea forma: | ||
- | * ''<s>,<c>,<d>'' unde ''<s>'' si ''<d>'' sunt intregi ce codifica starea //sursa// respectiv //destinatie//, iar ''<c>'' este un caracter (poate fi orice caracter cu exceptia '''\n''' si '','' | ||
- | |||
- | ==== Ce intoarce la output un lexer? ==== | ||
- | |||
- | Output-ul se va realiza intr-un fisier si va fi un sir de tokens si lexeme identificate. El va avea forma: | ||
- | <code> | ||
- | <token1> <lexeme1> | ||
- | ... | ||
- | <token_n> <lexeme_n> | ||
- | </code> | ||
- | |||
- | ==== Cum implementam un lexer? ==== | ||
- | |||
- | In linii mari, implementarea unui lexer este relativ simpla: dupa ce a citit lista AFD-urilor (in ordinea din fisier), acesta mentine configuratiile fiecarui AFD pe masura ce scaneaza input-ul. Implementarea lexerului necesita atentie din cauza unei proprietati importante a acestuia: | ||
- | * el va cauta intotdeauna **cel mai lung** sir care este acceptat de un AFD | ||
- | |||
- | Spre exemplu, daca avem token-urile (definite prin DFA-uri) pentru limbajele: | ||
- | * ZEROS ''0+'' | ||
- | * ONES ''1+'' | ||
- | * PAT ''0*10*'' | ||
- | |||
- | si input-ul ''00011000'', atunci lexer-ul va recunoaste input-ul ca fiind: ''(PAT,00011000)'' si nu: ''(ZEROS,000),(ONES,11),(ZEROS,000)''. | ||
- | |||
- | Pentru a intelege ratiunea acestei abordari, ganditi-va la urmatorul program: | ||
- | <code> | ||
- | varif = if x > 0 then 1 else 0 | ||
- | </code> | ||
- | |||
- | Daca token-urile au fost definite corect, programul trebuie sa identifice ''varif'' ca fiind o variabla, si nu o variabila urmata de cuvantul cheie ''if''. (Mai multe detalii in Anexa 1). | ||
- | |||
- | === Sink states === | ||
- | |||
- | Tocmai de aceea, in procesul de analiza lexicala, avem nevoie sa determinam, pentru fiecare AFD, **multimea sink-states** (deobicei, va fi una singura). Aceasta poate fi calculata //inversand// tranzitiile fiecarui AFD si vizitand toate starile accesibile din starea finala. Starile ce **nu** vor fi gasite astfel vor fi **sink-states**. | ||
- | |||
- | === Configuratii === | ||
- | |||
- | Spre deosebire de teoria de la curs, unde configuratiile erau perechi cuvant(ramas a fi scanat) si stare, pentru lexerul nostru configuratiile vor fi usor diferite. In primul rand, noi nu verificam daca un AFD accepta un cuvant, ci incercam sa spargem un sir in mai multe cuvinte (de lungime maximala), ce ar fi acceptate de unul din AFD-urile citite de la input. | ||
- | |||
- | De aceea, o configuratie pentru un AFD trebuie sa retina (intr-o forma sau alta): | ||
- | - starea curenta | ||
- | - ultima pozitie din input unde a acceptat (daca este cazul) | ||
- | - daca starea curenta este un sink-state sau nu. | ||
- | |||
- | Explicam necesitatea lor folosind un exemplu. Sa consideram token-urile (definite ca AFD-uri): | ||
- | * FST ''(11)+'' | ||
- | * SND ''(10)+'' | ||
- | |||
- | si input-ul ''1011''. Lexer-ul va evolua in felul urmator: | ||
- | * **primul caracter:** ''1''. Ambele AFD-uri vor citi ''1'' si se vor muta in starea succesor, asteptand urmatorul caracter. | ||
- | * **al doilea caracter:** ''0''. AFD-ul aferent FST va ajunge intr-un sink state (odata citit 0 nu mai putem accepta). Al doilea AFD va ajunge intr-o stare finala si in configuratia acestuia vom retine ca la pozitia 1 din sir am identificat un token. Scanarea insa continua, pentru ca acesta nu e in mod necesar cel mai lung lexem aferent token-ului. | ||
- | * **al treilea caracter:** ''1''. AFD-ul aferent FST ramane in sink state. AFD-ul aferent SND ajunge si el in sink-state, si cum **nu mai exista alte AFD-uri in stari non-sink**, lexer-ul va raporta token-ul avand **lexemul cel mai lung** care a acceptat anterior, adica cel aferent pozitiei 1: ''(SND,10)''. | ||
- | * **reset**: procesul de scanare va continua, cu doua modificari: | ||
- | * toate AFD-urile vor fi //re-aduse// in starea initiala | ||
- | * ne vom intoarce la input la pozitia **urmatoare** celei unde am raportat token-ul gasit anterior, adica **pozitia 2**. Asadar, sirul de la input va fi ''11'' | ||
- | * **revizitam al treilea caracter:** ''1''. Ambele AFD-uri trec intr-o noua stare; | ||
- | * **al patrulea caracter:** ''0''. Al doilea AFD trece intr-un sink state. Primul AFD trece intr-o stare finala. In mod normal, procesul de cautare ar continua, insa intalnim finalul sirului. Prin urmare, raportam ''(FST,11)''. | ||
- | |||
- | ===== Sugestii de implementare pentru Python ===== | ||
- | |||
- | ==== Pattern matching in Python ==== | ||
- | |||
- | Limbajul Python suporta un mecanism simplu de pattern matching pentru tupluri, care poate fi util in proiect. Ilustram cateva exemple: | ||
- | <code python> | ||
- | def f(): | ||
- | a,b,c = (1,2,3) | ||
- | return a,b,c | ||
- | </code> | ||
- | ''print(f())'' afiseaza ''(1,2,3)'' | ||
- | |||
- | <code python> | ||
- | def f(): | ||
- | a,b,*c = (1,2,3) | ||
- | return a,b,*c | ||
- | </code> | ||
- | ''print(f())'' afiseaza ''(1,2,3)'' | ||
- | |||
- | <code python> | ||
- | def f(): | ||
- | a,b,*c = (1,2) | ||
- | return a,b,*c | ||
- | </code> | ||
- | ''print(f())'' afiseaza ''(1,2)'' | ||
- | |||
- | Puteti testa voi insiva alte scenarii similare. | ||
- | |||
- | ==== Dictionare si hashing in Python ==== | ||
- | |||
- | Dictionarele din Python vor fi folosite extensiv in proiectul vostru, motiv pentru care este important sa intelegem proprietatea de a fi //hashable// a **cheilor** din acestea. | ||
- | |||
- | Dictionarele din Python sunt implementate ca **HashMaps** (tabele de dispersie), in care fiecare element este **hashed** (dispersat) intr-un bucket (o lista implementata intern), pentru stocare. | ||
- | |||
- | * Un **hash** (functie de dispersie) este cel mai **eficient** cand se comporta ca o functie **injectiva** (hash-ul fiecarui element este unic, deci fiecare element are propriul bucket). | ||
- | * Un **hash** este **corect** cand asociaza **intotdeauna** acelasi hash pentru acelasi element (se comporta ca o functie). | ||
- | |||
- | Obiectele **mutabile** din Python (precum listele, set-urile) **nu pot fi folosite drept chei** ale unui dictionar, din cauza ca, pe parcursul executiei programului, ele se pot modifica (putem insera un element nou intr-o lista), lucru care ar face ca hash-ul lor sa devina diferit. Prin urmare proprietatea a doua (corectitudinea) nu ar mai fi adevarate. | ||
- | |||
- | Putem folosi instante ale claselor drept chei ale dictionarelor, cu conditia sa implementam functia ''%%_%%_hash__()''. Exista numeroase resurse online despre cum poate fi realizat acest lucru. | ||
- | |||
- | |||
- | ==== Starile unui AFD ==== | ||
- | |||
- | In aceasta etapa a proiectului, AFD-urile citite de la input contin **stari codificate ca intregi**. In implementarea structurii de date care va reprezenta AFD-uri trebuie sa **anticipam** urmatoarele: | ||
- | * In alte etape ale proiectului vom folosi AFD-uri in care **starile** sunt codificate **altfel** (multimi de intregi). | ||
- | * De asemenea, vom implementa AFN-uri - o structura de date foarte asemanatoare cu AFD-uri si cu functionalitati comune. | ||
- | * In cadrul AFN-urilor, starile trebuie sa suporte **transformari** (sa expuna o functie asemanatoare cu ''map''), care sa nu modifice functionalitatea acestora. | ||
- | |||
- | In aceasta etapa a proiectului, puteti: | ||
- | - alege o implementare simpla de AFD (e.g. stari codificate ca intregi impreuna cu un dictionar). Aceasta va trebui sa fie usor de modificat pentru urmatoarele etape a proiectului | ||
- | - pregati o implementare de AFD care sa permita cele de mai sus, si a carei implementare va fi rafinata in urmatoarele etape. Oferim cateva sugestii: | ||
- | - implementarea unei clase ''Stare'', care poate fi modificata (sau extinsa) ulterior | ||
- | - codificarea starilor printr-o lista: ''[s1, s2, ..., sn]'', in care elementele pot avea orice tip (profitam astfel de flexibilitatea typing-ului in Python). Dictionarul AFD-ului va folosi indecsii acestei liste si nu valorile efective. In felul asta, putem transforma transparent de dictionar valorile starilor, si evitam problema hashing-ului. | ||
- | |||
- | ==== Igiena codului ==== | ||
- | |||
- | In multe situatii, in implementarea unei operatii generale, avem nevoie de a izola operatii locale, insa acestea au nevoie de intregul context pentru implementare. Spre exemplu: | ||
- | |||
- | <code Python> | ||
- | def smallOperation(x,y,z): | ||
- | ... | ||
- | def tinyOperation(x,y): | ||
- | ... | ||
- | def bigOperation(data1, data2, data3): | ||
- | chunk1 = smallOperation(0,data1,data2) | ||
- | chunk2 = tinyOperation(data2,data3) | ||
- | ... | ||
- | </code> | ||
- | |||
- | Puteti evita pasarea contextului global fiecarei operatii, definind functii in functii (suportate in Python). Acestea vor avea vizibil intregul context. Astfel, nu mai mutam date redundant in apeluri de functii, iar codul este mai usor de urmarit: | ||
- | |||
- | <code Python> | ||
- | def bigOperation(data1, data2, data3): | ||
- | def smallOperation(x): | ||
- | ... | ||
- | def tinyOperation(): | ||
- | ... | ||
- | chunk1 = smallOperation(0) | ||
- | chunk2 = tinyOperation() | ||
- | ... | ||
- | </code> | ||
- | |||
- | ===== Sugestii de implementare pentru Haskell ===== | ||
- | |||
- | |||
- | ==== Tipuri ==== | ||
- | |||
- | Pentru a putea integra mai usor lexer-ul vostru cu alte componente pe care le vom implementa ulterior, folositi urmatoarea clasa (la care vom adauga mai tarziu, definitii), care modeleaza **Automate Finite** (nu neaparat deterministe): | ||
- | |||
- | <code Haskell> | ||
- | class FA t where | ||
- | fromList :: State s => Set Char -> s -> [(s,Char,s)] -> [s] -> t s | ||
- | states :: (State s) => t s -> [s] | ||
- | </code> | ||
- | |||
- | In anticiparea diverselor transformari care vor fi implementate peste AFD-uri, este important ca **starile** acestora sa fie **polimorfice**: | ||
- | <code Haskell> | ||
- | type Delta a = Map (a,Char) a | ||
- | data Dfa a = Dfa {sigma :: Set Char, initial :: a, delta :: (Delta a), fin :: [a]} | ||
- | </code> | ||
- | |||
- | Definitia pentru ''Delta'' foloseste ''Maps'' care sunt cele mai usor de folosit in acest context. | ||
- | |||
- | Inrolati //containerul// ''Dfa :: * => *'' in clasa ''FA''. | ||
- | |||
- | ==== Citirea input-ului ==== | ||
- | |||
- | Parcurgerea input-ului linie cu line se implementeaza greoi in Haskell, insa separarea AFD-urilor poate fi realizata usor, plecand de la urmatoarea observatie. Daca impartim folosind caracterul '\n' o portiune din input, vom obtine: | ||
- | |||
- | <code> | ||
- | [ <linie1>, ... <linie_k>, "", <linie_k+1> , .... <linie_k+n>, "", ...] | ||
- | </code> | ||
- | Daca aplicam aceeasi functie de impartire (cu signatura rescrisa polimorfic) folosind //obiectul// "", obtinem o lista de liste de linii, unde fiecare lista de linii reprezinta codificarea unui AFD. | ||
- | |||
- | ==== Organizarea codului ==== | ||
- | |||
- | === Tipuri intuitive === | ||
- | Signaturile functiilor voastre pot deveni lungi. Va recomandam sa le scrieti voi insiva (va ajuta la implementarea corecta), si sa introduceti, de fiecare data cand simtiti nevoia, //type-def-uri// menite sa faca citirea codului mai usoara. Exemplu: | ||
- | |||
- | <code Haskell> | ||
- | type Index = Integer | ||
- | type State = Integer | ||
- | |||
- | f :: State -> Index -> ... | ||
- | f = ... | ||
- | </code> | ||
- | |||
- | === Igiena codului === | ||
- | |||
- | Cautati sa evitati implementarile //mamut// in detrimentul izolarii functionalitatii in functii simple, cu signaturi scrise de voi, si cu comentarii intuitive. De multe ori, functiile pot fi atat de scurte, incat pot fi scrise inline. Va incurajam sa folositi ''$'' impreuna cu ''.'' si sa formatati cat mai intuitiv codul. | ||
- | |||
- | Exemplu negativ: | ||
- | <code Haskell> | ||
- | f l = zipWith (\x y-> (x:y)) (head (take 20 ((reverse (tail l)))) (tail (reverse l)) | ||
- | </code> | ||
- | |||
- | Exemplu pozitiv: | ||
- | <code Haskell> | ||
- | f l = zipWith (:) l1 l2 | ||
- | where l1 = head -- i am extracting the first element of the list because... | ||
- | $ (take 20) -- i only take the first 20 elements because ... | ||
- | $ reverse -- the list must be reversed | ||
- | $ tail l | ||
- | l2 = (tail . reverse) l -- i am composing two transformations over l | ||
- | |||
- | |||
- | </code> | ||
- | |||
- | === Helper functions === | ||
- | |||
- | * Vizitati functiile din biblioteca ''Data.Map'' si ''Data.Set''. Unele pot fi foarte utile pentru implementare | ||
- | * Refolositi functia ''splitBy'' de la PP | ||
- | * Aruncati o privire peste functia ''sortOn''. Este mai simplu de folosit si mai performanta decat ''sortBy''. | ||
- | * Aruncati o privire peste **list comprehensions**. Pot fi utile local. | ||
- | |||
- | ==== Debugging ==== | ||
- | |||
- | Cel mai probabil, la baza implementarii voastra va sta o functie de forma: | ||
- | <code Haskell> | ||
- | lexer :: InputString -> [TokensAndTheirDfas] -> Output | ||
- | </code> | ||
- | |||
- | care va fi **tail-recursive**, si la tipul careia veti adauga diverse alte componente/acumulatori. Debugging-ul acesteia poate fi dificil, in special daca functia cicleaza. O modalitate simpla de debugging este sa adaugati functiei voastre doua variabile, modificand output-ul astfel: | ||
- | |||
- | <code Haskell> | ||
- | auxLexer :: InputString -> [TokensAndTheirDfas] -> String -> Integer -> (String,Output) | ||
- | auxLexer _ _ _ 100 = ... | ||
- | </code> | ||
- | |||
- | unde: | ||
- | * variabila de tip String va codifica elemente de logging utile pentru debugging (e.g. starea sirului, starile curente ale AFD-urilor). La fiecare apel recursiv puteti adauga informatii la logging pentru a urmari mai usor executia functiei. La final, puteti intoarce informatiile de logging, impreuna cu outputul. | ||
- | * variabila de tip Integer va reprezenta numarul de apeluri recursive. Cand o limita (hardcodata) este atinsa, functia intoarce indiferent de valoarea parametrilor. In felul asta, puteti inspecta logging-ul si in situatia cand functia cicleaza. Nu uitati sa incrementati la fiecare apel recursiv variabila in cauza. | ||
- | * puteti pastra aceasta forma si in implementarea voastra finala ( //imbracand// functia ''auxLexer'' cu o alta functie care //ascunde// detaliile de logging) | ||
- | |||
- | |||
- | ===== Anexa 1 ===== | ||
- | |||
- | Revenind la exemplul anterior, am putea incerca sa rezolvam problema identificarii folosind **ordinea** in care sunt definite tokens. Observati insa ca, in acest exemplu, nici o ordine a tokenilor pentru variabila si ''if'' nu ne vor ajuta sa scanam corect inputul (daca regula pentru if e prioritara, se repeta situatia descrisa anterior, iar daca regula pentru variabile e prioritara, if va fi scanat ca o variabila (probabil functie). | ||
- | <code> | ||
- | varif = if x > 0 then 1 else 0 | ||
- | </code> | ||
+ | * **[[lfa:proiect:etapa1 | Etapa 1 - Lexer cu AFD-uri]]** | ||
+ | * **[[lfa:proiect:etapa2 | Etapa 2 - Transformare Regex - AFD]]** | ||
+ | * **[[lfa:proiect:etapa3 | Etapa 3 - Lexer cu Regexuri si parser]]** | ||
+ | * **[[lfa:proiect:checker | Checker]]** | ||