This is an old revision of the document!


Schelet si checker pentru fiecare limbaj:

Proiect

Proiectul consta in implementarea unui lexer in python sau scala.

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;

...

Fiecare TOKEN N este un nume dat unui tip de token, iar REGEX N este un regex ce descrie lexemele ce pot fi clasificate ca acel token.

Apoi lexer-ul va primi un text, care va fi impartit in lexeme pe baza specificatiei primite anterior, dupa un set de reguli ce vor fi descrise in cursuri ulterioare si vor fi acoperite mai tarziu in proiect.

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.

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 AFD pe baza specificatiei pentru analiza eficienta a textului.

In aceasta etapa a proiectului veti implementa conversia unei versiuni de regex mai usor de parsat, numita Prenex, in AFN, apoi din acest AFN in AFD. Parsarea expresiilor “standard” regex va fi amanata pentru etapa viitoare din cauza dificultatii de generare a arborilor expresiilor.

Prenex

Expresiile regulate in forma Prenex sunt formate fie din atomi (un caracter alfanumeric, un caracter oarecare inclus intre ghilimele simple (de exemplu, ' ' ) sau cuvintele cheie eps (pentru sirul vid) si void (pentru limbajul vid)), fie din numele operatiilor (UNION, STAR, CONCAT, PLUS, MAYBE) urmate direct de operanzii acestora (reprezentat de o alta subexpresie in forma Prenex).

Urmatoarele sunt exemple valide de expresii Prenex:

  • UNION a b, echivalent cu a | b
  • UNION CONCAT a b STAR c , echivalent cu (ab)|(c*)
  • CONCAT UNION a b UNION c d, echivalent cu (a|b)(c|d)
  • CONCAT STAR UNION a b UNION b c, echivalent cu (a|b)*(b|c)
  • STAR UNION CONCAT a b CONCAT b STAR d, echivalent cu ( (ab) | ( b(d*) ) )*
  • CONCAT PLUS c UNION a PLUS b, echivalent cu c+( a| (b+) )
  • UNION ' ' '@' ; STAR '#'
  • UNION eps a, MAYBE a, echivalente cu a|ε
  • void

Pentru implementarea acestei etape, se va implementa parsarea expresiei prenex (se recomanda folosirea unei structuri interne arborescente ca rezultat al parsarii, dar reprezentarea exacta a acestei structuri este la latitudinea voastra) si conversiile Prenex → AFN → AFD.

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, implementarea claselor DFA si NFA trebuie sa fie generica in raport cu tipul de date prin care reprezentam starile.

In plus, in anumite momente este foarte posibl sa fie nevoie sa redenumim starile unui anumit automat. In acest caz, este recomandata implementarea unei functionalitati de tip map care sa faca aceasta redenumire dupa o anumita functie (primita ca parametru), fara a modifica comportamentul automatului.

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.

Verificarea corectitudinii implementarii voastre se va face automat, printr-o serie de teste unitare, o parte punctate si o parte nepunctate. Aceste teste nu acopera fiecare caz posibil si testeaza doar comportarea corecta a AFD-urilor si AFN-urilor obtinute din cateva expresii in forma prenex, pe cateva secvente reprezentative.

Sunteti incurajati sa va adaugati propriile teste, fie pentru a asigura corectitudinea pe mai multe cazuri sau pentru a testa alte componente intermediare (de exemplu parsarea corecta a expresiilor in forma prenex si construirea unui arbore corect pentru acestea).

Folderul care contine testele va fi suprascris de checker, testele luate in considerare fiind cele din skeletul de cod

Python

Pentru rularea testelor folositi comanda python3 -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.

Pentru a va defini propriile teste, creati o noua clasa in folderul test care sa extinda clasa unittest.TestCase si creati cate o metoda pentru fiecare test. Numele acestor metode trebuie sa inceapa cu test pentru a fi recunoscute ca fiind cazuri de testare. Pentru a indica comportamentul testat de fiecare test putem folosi metodele de tipul self.assert…(). Unele dintre cele mai frecvent folosite astfel de metode sunt:

  • self.assertTrue(expression_expected_to_be_true) si self.assertFalse(expression_expected_to_be_false)
  • self.assertEqual(expression, expected_value_of_expression)
  • self.assertIn(expression, list_of_possible_expected_values_of_expression)

Daca in cadrul unui test vreuna din asertii nu este indeplinita cazul de test este marcat ca esuat.

Scala

Pentru rularea testelor folositi comanda sbt test.

Aceasta comanda va rula testele definite in folderul src/test/scala si va afisa cu verde testele terminate cu succes si cu rosu testele esuate.

Pentru definirea propriilor teste, creati o noua clasa in folderul src/test/scala care sa extinda clasa munit.FunSuite, in corpul careia puteti sa adaugati oricate teste sub forma:

test("nume test") {
    // instructiuni si asertii
    assert(booleanValue) // -> testul va esua daca booleanValue se evalueaza la fals
}
In radacina proiectului trebuie pus un fisier intitulat ID.txt ce va avea pe prima linie a sa ID-ul vostru anonim (ar trebui sa il fi primit pe mail, dar daca din vreun motiv nu il aveti, vorbiti cu asistenul de laborator) si pe a doua linie limbajul in care rezolvati tema (python sau scala)

De exemplu:

9921225
scala

sau

9246163
python

Structura arhivei (Python)

.
├── ID.txt
└── src
    ├── DFA.py
    ├── __init__.py
    ├── NFA.py
    ... (alte surse pe care le folositi)

Structura arhivei (Scala)

.
├── build.sbt
├── ID.txt
└── src
    └── main
        └── scala
            ├── Dfa.scala
            ├── Nfa.scala
            ... (alte surse pe care le folositi)
Pentru niciunul din limbaje nu este necesar sa includeti folder-ul cu teste, dar includerea sa nu va cauza erori.