Etapa 3 - Lexer complet si Limbajul Imperative

Checker si teste: Checker proiect LFA.

Etapa 3 consta la randul ei in trei parti, dintre care ultima este bonus.

Punctaj

  • Punctajul pentru 3.1. este de 0.7p (din totalul de 1p al etapei)
  • Punctajul pentru 3.2. este de 0.3p (din totalul de 1p al etapei)
  • Punctajul pentru 3.3. este de 0.3p (bonus la Etapa 3)

Le descriem in continuare pe fiecare dintre ele:

Plecand de la implementarile voastre pentru etapele 1 si 2, veti realiza un lexer complet care, spre deosebire de lexer-ul de la Etapa 1, primeste o specificatie de tokens ce contine expresii regulate in loc de AFD-uri.

Daca nu ati citit pana acum cerinta de la Etapa 1, este bine sa o faceti pentru a intelege mai bine Etapa 3.

Structura fisierului cu tokens

Fisierul va contine un token si expresia lui regulata, cate una pe fiecare linie, sub urmatoarea forma:

<token> <regex>;

unde:

  • un <token> este o secventa de caractere alfabetice (de obicei uppercase)
  • un <regex> este o expresie regulata

Token-ul este separat printr-un spatiu alb de expresia regulata. Fiecare linie se va termina prin caracterul special ;.

Expresia regulata are urmatoarea sintaxa:

<regex> ::= <regex><regex> |       # concatenare
            <regex> | <regex> |    # reuniune (intre regex-uri folosim caracterul '|' pt a desemna reuniunea)
            <regex>*  |            # Kleene star
            <regex>+  |            # ee*
            (<regex>) |            # paranteze
            [a-z]+ |               # alias pt expresia a | b | c ... | z, avand numai caractere alfabetice lowercase
            [0-9]+ |               # alias pt expresia 0 | 1 | 2 | ... | 9
            <c>    |               # orice caracter alfanumeric
            '<any_c>'              # orice caracter ASCII, nu doar unul alfabetic            

Spatiile albe pot aparea liberal in corpul unei expresii regulate, fara a influenta sensul acesteia. Expresiile regulata ce contin caracterul whitespace se regasesc incadrate intre ' ' .

Exemplu de specificatie:

KEYWORD def | while | if;            # concatenare de caractere
REGISTER R[0-9]+;                    # orice sir ce incepe cu caracterul R urmat de unul sau mai multe cifre
EXPR [a-z]+ (('+' | '*') [a-z]+)*;   # o posibila codificare a expresiilor aritmetice
SPACES ' ' | '\t';                   # spatii albe

Implementarea lexerului complet

Aceasta poate fi organizata astfel:

  • Citirea si parsarea specificatiei: aceasta va fi contributia cea mai substantiala a acestei etape. Expresiile regulate nu mai sunt in forma PRENEX, asadar, pentru parsarea lor va trebui sa implementati un APD (Automat Push-Down) simplu. Aceasta va construi un arbore al expresiei (poate fi aceeasi structura de date folosita la Etapa 2).
  • Generarea de AFD-uri, cate unul pt fiecare expresie regulata. Pentru aceasta veti prelua implementarea voastra a etapei Etapa 2.
  • Implementarea procedurii de analiza lexicala: veti citi un fisier, si veti construi o lista de lexeme, exact ca la Etapa 1. Puteti folosi direct implementarea voastra anterioara.

Output si testare

Output-ul va fi scris intr-un fisier, fiind unui sir de tokens si lexeme identificat (la fel ca la Etapa 1). El va avea forma:

<token1> <lexeme1>
...
<token_n> <lexeme_n>

unde fiecare pereche <token,lexem> se afla pe cate o linie separata, iar intre fiecare token si lexem se afla cate un singur spatiu alb (neincluzand spatiile albe ce se pot afla in compozitia lexemului).

Testarea va fi efectuata intr-o maniera identica ca cea din cadrul Etapei 1, prin compararea output-ului generat de voi cu cel aflat in fisierele de referinta. Mai multe detalii legate de script-ul de testare si cum sa il rulati gasiti pe pagina dedicata Checker-ului.

Sugestii de implementare pt Lexer complet

  • Inainte de a scrie cod, construiti o gramatica ne-ambigua pentru limbajul expresiilor regulate. Atentie la precedenta operatorilor.
  • Folositi ideile din aceasta gramatica pentru a construi un APD (Automat Push-Down).
  • Comportamentul APD-ului scris de voi este cel mai bun road-map pentru implementare. Stiva APD-ului poate fi folosita inclusiv pt a stoca referinte la expresiile regulate partiale pe care le-ati parsat.

Folosind lexer-ul scris de voi, implementati un parser simplu pentru limbajul Imperative descris mai jos. Folositi aceeasi abordare ca in implementarea parserului pentru expresii regulate. }

Input

Input-ul va fi un program a carui descriere sintactica se regaseste mai jos (Limbajul Imperative):

<prog> ::= <variable> '=' <expr>
           | begin <instruction_list> end
           | while (<expr>) do <prog> od
           | if (<expr>) then <prog> else <prog> fi 

<instruction_list> ::= <prog> | <prog> '\n' <instruction_list>   

<expr> ::= <expr> '+' <expr> | <expr> '-' <expr> | <expr> '*' <expr> | <expr> '>' <expr> | <expr> '==' <expr> | <variable> | <integer> 
                   
  • Urmariti structura fisierelor de test pentru a intelege mai bine gramatica precum si semnificatia variabilelor si a intregilor.
  • Acordati atentie modului in care sunt folosite \n in sintaxa limbajului (atunci cand delimiteaza intre instructiuni si atunci cand doar formateaza).

Pentru implementare, trebuie sa:

  • scrieti o specificatie pentru analiza lexicala a programelor;
  • folosind prima parte a etapei 3, implementati un parser pentru programe Imperative. Scopul vostru este sa realizati o parsare corecta si o afisare a acesteia. Pentru aceasta din urma, puteti folosi urmatorul schelet de clase, care vor reprezenta AST (Abstract Syntax Tree-ul) pentru programe Imperative si care au deja implementata pentru voi procedura de afisare: ast.zip

Output

Output-ul va fi redat sub forma unui fisier ce contine AST-ul rezultat in urma parsarii programului. Un exemplu de instantiere si afisare se gaseste in comentariile din scheletul de clase ast.py (atentie: subblocurile sunt identate cu cate doua spatii fata de blocurile parinte).

Folosind parserul scris anterior, realizati un interpretor pentru programe Imperative. Interpretorul va folosi AST-ul scris de voi. Aceasta parte nu va avea tester dedicat si se va baza pe fisierele de testare ale partii 3.2. In timpul prezentarii veti ilustra asistentului vostru modalitatea de functionare.

In termeni generici, un interpretor mentine si gestioneaza un store, adica o mapare intre nume de variabile intalnite in program, si valorile la care sunt legate acestea:

  • anumite instructiuni adauga noi legari variabila-valoare la store
  • anumite instructiuni modifica legari existente
  • anumite instructiuni pot doar verifica valorile respective pentru a lua o decizie.
  • Interpretorul va incepe intotdeauna executia cu un store vid.
  • Output-ul unui interpretor va fi continutul store-ului final, mai exact, maparea ce contine toate variabilele folosite in program alaturi de valorile lor.

Mai jos aveti un exemplu de actualizare a unui store in timpul interpretarii unui program scurt (store-ul este reprezentat in comentarii drept un dictionar ce retine maparile dintre o variabila si valorea sa curenta):

begin  // {} - store vid
a = 1; // {a : 1} - s-a adaugat o mapare in store
r = 0; // {a : 1, r : 0}
if (a == 2) then // {a : 1, r : 0} - expresiile booleene nu modifica store-ul
    r = a + 3;   // nu se ajunge aici
else
    r = a; // {a : 1, r : 1} - valoarea lui r a fost actualizata
fi
end

Sugestii de implementare pentru limbajul Python

  • Cel mai indicat mod de implementare a unui interpretor intr-un limbaj Orientat-Obiect este folosind design pattern-ul Visitor, in care:
    • AST-ul este structura vizitata (Visitable)
    • Interpretorul este vizitatorul (Visitor)

Sugestii de implementare pentru limbajul Haskell

  • Cel mai indicat mod de implementare a unui interpretor intr-un limbaj functional este folosind Monade; Intrucat acestea nu au fost discutate inca, puteti folosi o implementare direct-recursiva, care sa construiasca o functie cu signatura Program → Store , unde Store trebuie definit de catre voi.

Citirea unui fisier in limbajul Haskell

IO (Input / Output) in limbajul Haskell se bazeaza pe design pattern-ul Monad, ne-discutat inca. Puteti totusi folosi cu usurinta urmatorul exemplu de cod pentru citirea dintr-un fisier, si afisarea unui mesaj:

test = do 
        in <- readFile "filename.extension"
        putStrLn $ show $ f in

In codul de mai sus:

  1. in este o variabila de tip String ce va retine continutul fisierului indicat
  2. putStrLn este o functie care are ca efect afisarea
  3. f :: (Show a) ⇒ String → a este o functie pe care o puteti defini voi sub ce forma doriti, si care materializeaza intreaba prelucrare pe care o faceti asupra continutului fisierului. Ea poate intoarce orice obiect, cu conditia sa fie afisabil (astfel incat apelul show sa fie unul valid).