În acest laborator vom aprofunda cunoștințele de bison și vom incepe să discutăm despre prima temă.
Să reluăm cum comunică bison-ul cu flex-ul în vederea generării parser-ului.
În fișierul reprezentând input-ul pentru bison
vom scrie funcția main()
, în care vom apela yyparse()
. Funcția yyparse()
este deja implementată și o vom regăsi în fișierul .c al parserului.
yyparse()
citește un șir de perechi token/valoare
pe care le primește de la yylex()
.
yylex()
citește caracterele dintr-un file pointer (FILE*) numit yyin. Dacă nu setați yyin, acesta va pointa către intrarea standard (stdin). Rezultatul este transmis către yyout, care, implicit, pointează către ieșirea standard (stdout).
Fiecare apel către yylex()
returnează un întreg, care reprezintă tipul token-ului (id unic). Astfel, bison-ul știe ce token a primit. Token-ul poate avea o valoare, care trebuie să fie pusă în variabila yylval
.
Implicit, yylval este de tip int
, tip care, însă, se poate schimba redefinind YYSTYPE în fișierul bison.
Structura fișierului de specificații o putem vedea ca fiind compusă din 3 zone:
Vom detalia partea de declarații. Structura clasică/veche/inflexibilă a zonei de declarații :
%{ #include <stdio.h> %} %union { .... }
Există situații în care avem nevoie de cod C în partea de declarații și după zona de definiții bison.
Pentru a nu exista confuzii, s-a adăugat %code
care poate fi urmat de calificatori:
Directiva %code {code} | Insereaza code în fișierul implementării parserului, intr-o zonă dependentă de limbaj |
Directiva %code qualifier {code} | Inserează codul într-o anumită locație, așa cum vom vedea mai jos |
Calificator | Semnificație |
---|---|
requires | Scop: Aici se scrie codul de care depind YYSTYPE și YYLTYPE. Altfel spus, aici se definesc tipurile referite în directiva %union . Dacă folosiți #define pentru a rescrie valorile implicite date de Bison pentru YYSTYPE și YYLTYPE, tot aici îl veți pune. Locație: Fișierul header al parserului și fișierul .c înainte de definițiile implicite ale Bison-ului pentru YYSTYPE si YYLTYPE. |
provides | Scop: Aici se pun definiții și declarații auxiliare, ce sunt necesare altor module. Locație: În fișierul header al parserului și în fișierul .c, după definițiile implicite ale Bison-ului pentru YYSTYPE și YYLTYPE și după definiția tokenilor |
top | Scop: %code fără calificator și %code requires sunt de cele mai multe ori mai potrivite decat %top. Există, totuși, situații în care este necesar să inserăm cod chiar la începutul fisierului .c generat. De exemplu: %code top { #define _GNU_SOURCE #include <stdio.h> } Locatie: La inceputul fisierului .c generat. |
imports | folosit pentru Java |
Uneori este util să punem acțiuni în mijlocul regulilor. Aceste acțiuni se scriu la fel ca acțiunile de la sfârșitul regulilor, dar sunt executate înainte ca parser-ul să recunoască componentele situate după ele.
O acțiune în mijlocul unei reguli poate să refere componentele care o preced folosind $N
, dar nu poate să le refere pe cele care o succed (pentru că nu au fost recunoscute în momentul în care se execută acțiunea).
Acțiunea din mijlocul regulii reprezintă ea însăși o componentă a regulii. Aceasta contează când regula are atât o acțiune în mijloc, cât și una la sfârșit. În acest caz, cea de la sfârșit trebuie să numere și acțiunea din mijlocul regulii.
Este posibil să asociem o valoare acțiunii din mijlocul regulii. Aceasta se face printr-o atribuire către $$
, iar acțiunile regulii, care o succed, pot să-i refere valoarea cu $<tip>N
. Avem nevoie de <tip> pentru că acesta nu a fost/nu poate fi declarat.
$$
setează valoarea acțiunii curente. Singura metodă de a atribui o valoare întregii reguli este prin acțiunile de la sfârșitul regulii.
Regula de mai jos parsează o instructiune de tip let
,
let (VARIABLE) STATEMENT
care creează o variabilă temporară, VARIABLE, cu scopul STATEMENT
.
Așadar, trebuie să punem variabila în tabela de simboli înaine ca STATEMENT să fie parsat și trebuie să o scoatem din tabelă după parsare.
stmt: LET '(' var ')' { $$ = push_context (); declare_variable ($3); } stmt { $$ = $6; pop_context ($5); }
După ce s-a recunoscut LET '(' var ')'
, este executată prima acțiune. Se salvează o copie a contextului curent (lista variabilelor disponibile). Apoi, se apelează declare_variable
care adaugă var
la lista curentă. În acest moment s-a terminat execuția acțiunii din mijlocul regulii, și se va parsa stmt
. Această acțiune este componenta numărul 5 a regulii, iar stmt
este componenta numărul 6. Dupa ce s-a parsat și stmt
se execută acțiunea de la sfârșitul regulii, care scoate variabila din tabela de simboli.
CMake este un meta-sistem de build independent de platformă. Acesta citeste niște fișiere de configurare (CMakeLists.txt
) care descriu procesul de build într-un limbaj specific, și pe baza acestora generează toate fișierele necesare pentru a face build-ul folosind un anumit tool: make, Ninja, Eclipse, Visual Studio etc. Pentru a vedea sistemele de build suportate pe o anumită platformă, puteți rula cmake -help
(secțiunea Generators
).
CMake poate fi folosit fie din linie de comandă, fie cu ajutorul unor wrappere cu interfață grafică (de exemplu ccmake
din pachetul cmake-curses-gui
). În cadrul laboratorului vom folosi interfața din linie de comandă.
cmake [OPTIONS] /path/to/source/tree
Printre opțiunile cele mai utile sunt:
cmake -G “Eclipse CDT4 - Ninja” /path/to/source/tree
)
Variabilele utilizate de fișierele de configurare pot fi fie predefinite (de exemplu cmake -DCMAKE_BUILD_TYPE=Debug
), fie specifice proiectului (de exemplu cmake -DSOMETHING_THAT_DETERMINES_HOW_MY_PROJECT_IS_BUILT=magic
); observați lipsa spațiului între -D și numele variabilei (ca la definițiile pentru preprocesor).
Fișierele generate se vor afla în directorul de unde a fost rulat CMake. Este recomandat ca acesta să fie diferit de cel în care se află sursele proiectului.
În această secțiune vom prezenta un subset al limbajului LCPL, atât cât este necesar pentru rezolvarea exercițiului din laborator. Descrierea completă a limbajului o veți primi în prima temă.
Codul LCPL este organizat in clase, similar cu Java si C++. Un program LCPL este definit în întregime într-un fișier. Un fișier poate conține mai multe clase. Definiția unei clase este: ([…] reprezintă construcții opționale)
class <nume> [inherits <nume>] <membri> end;
Clasele conțin zero sau mai mulți membri ce pot fi atribute sau metode.
Un atribut reprezintă date interne clasei și nu poate fi accesat direct decât din interiorul clasei. Pentru accesul din exterior se vor folosi metodele. Declarația unui atribut are forma:
<tip> <nume> [= <expresie>];
Atributele unei clase se declară in secțiunea var … end;
. Pot exista mai multe asemenea secțiuni într-o clasă. Atributele definite într-o secțiune sunt vizibile pe toată lungimea clasei (chiar și înainte de apariția în text a secțiunii în care au fost definite). Fiecare atribut are un tip care trebuie declarat explicit de programator și poate fi declarat împreună cu o inițializare.
var Factorial f = new Factorial; end;
Declarația unei metode are forma:
<nume> [<argumente>] [-> <tip>] : <corp> end;
Tipul unei metode reprezintă lista argumentelor acesteia, separate prin caracterul ',', precum și tipul întors de metodă. În cazul în care metoda nu întoarce o expresie, tipul întors lipsește din definiția metodei. În cazul în care metoda nu are argumente, lista lor lipsește din definiția metodei.
# metoda cu un argument si care intoarce un rezultat de tip Int fact Int n -> Int : if n < 1 then 1; else n * [fact n-1]; end; end; # metoda fara argumente si care nu intoarce nici un rezultat main : [out [f.fact 10]]; end;
Tipurile de date din LCPL sunt numerele întregi (Int
) și clasele. O clasă poate moșteni de la o singură superclasă (folosind inherits
urmat de numele clasei părinte). Numele unei clase este vizibil în tot programul.
Există trei clase speciale in LCPL: Object
, IO
și String
.
Clasa Object
este rădăcina ierarhiei de clase. Pe ea sunt definite următoarele metode:
# termina fortat programul abort # intoarce numele clasei originale a obiectului typeName -> String # # realizeaza o copie de suprafata a obiectului copy -> Object
Pentru a avea acces la standard input și output se vor folosi metodele din clasa IO
, prin intermediul unui obiect de acest tip. Metodele definite pe clasa IO
sunt următoarele:
# tipareste un sir la standard output out String message -> IO # citeste standard input pana la sfarsitul liniei in -> String
Clasa String
reprezintă șirurile de caractere. Metodele ei sunt:
# intoarce lungimea sirului de caractere length -> Int # converteste un sir de caractere la un intreg toInt -> Int
Corpul unei metode este format dintr-un bloc de instrucțiuni. Acestea pot fi expresii aritmetice, logice sau pe șiruri, instanțieri de obiecte, apeluri de metode, atribuiri și instrucțiuni de control. Toate instrucțiunile ce apar în corpul unei metode sunt terminate prin ';'.
Dacă o metodă întoarce o valoare, corpul acesteia trebuie să se termine cu o intstrucțiune al cărei tip corespunde celui întors de metodă. De exemplu, metoda fact
de mai sus întoarce valoarea instrucțiunii if
, de tip Int
.
Cele mai simple expresii din limbaj sunt constantele. Ele pot fi întregi sau șiruri de caractere.
Apelul unei metode, dispatch
, se realizează folosind una din construcțiile:
[<expr>.<id> <expr> ... <expr>] # apel de metoda cu argumente pe obiectul care este rezultatul <expr> [<expr>.<id>] # apel de metoda fara argumente pe obiectul care este rezultatul <expr> [<id> <expr> ... <expr>] # apel de metoda cu argumente pe obiectul curent [<id>] # apel de metoda fara argumente pe obiectul curent
La un apel de metodă cu argumente, acestea vor fi separate prin spațiu de celelalte.
O expresie condiționată este de forma:
if <expr> then <expr>; ... <expr>; else <expr>; ... <expr>; end; if <expr> then <expr>; ... <expr>; end;
Pentru a construi și inițializa un nou obiect, se folosește new
:
Factorial f = new Factorial;
Operațiile aritmetice ('+', '-', '*', '/'), de comparație ('<', '⇐') și de egalitate ('==') sunt expresii. Operatorul '-' funcționează și ca operator unar, pentru a obține un număr negativ.
Pentru a nega un întreg se folosește '!'.
Pentru a grupa expresii se folosesc paranteze: '(' și ')'.
LCPL conține următorii atomi lexicali:
Comentariile sunt doar pe o singură linie, încep cu #
și se termină la finalul liniei.
Cunvinte cheie: class, inherits, if, then, else, end, new, var
.
În rezolvarea laboratorului folosiți arhiva de sarcini lab03_mini_lcpl_parser.zip
În directorul mini-lcpl-parser
există două sub-directoare:
make
. În urma compilării rezultă executabilul lcpl-parser
./lcpl-parser path_fisier
Ierarhia de clase care reprezintă noduri ale AST-ului este formată dupa cum urmează (din lcpl-AST/include/):
Ulterior rulării ”./lcpl-parser simple.lcpl”, urmăriți cum a fost parsat conținutul fișierului simple.lcpl și AST-ul rezultat (pe acesta îl găsiți în simple.lcpl.ast).
Va trebui să completați analizorul lexical astfel încât să recunoască și să paseze către analizorul sintactic următorii atomi lexicali:
Pentru fiecare nod nou adaugat urmăriți modul de inițializare al acestora din folderul lcpl-AST/include.
Pentru operatorii '-', '*', '<' și metode cu valoare de retur va trebui să adaugați reguli de parsare și noi noduri, de tipul 'BinaryOperator', respectiv 'Method', în arbore. De asemenea, adaugați regula de parsare . Urmăriți exemplul pentru operatorul '+', respectiv metoda fara valoare de retur, și indicațiile marcate cu “TODO 1”.
Pentru statementul if va trebui să adaugați o nouă regulă de parsare și nodul aferent, de tipul 'IfStatement', în arbore. Pentru simplitate regula va trebui să evalueze doar structuri de forma: if <cond> then <expr> else <expr> end. De asemenea va trebui să definiți tipul intors de această regulă. Urmăriți indicațiile marcate cu “TODO 2”.
Adaugați reguli de parsare și nodurile aferente pentru apelul de funcție ('Dispatch') de forma : [id expr1 expr2 …], de asemenea va trebui să definiți tipul intors de aceste reguli. Urmăriți indicațiile marcate cu “TODO 3”.
Extindeți exercițiul anterior, astfel încât gramatica sa accepte asignarea variabilelor și moștenirea unei clase. Urmăriți indicațiile marcate cu “TODO BONUS”.