03. Bison Advanced

În acest laborator vom aprofunda cunoștințele de bison și vom incepe să discutăm despre prima temă.

Bison

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

Structura fișierului de specificații o putem vedea ca fiind compusă din 3 zone:

  • Declarații (C și bison)
  • Regulile gramaticii
  • Cod C

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

Mai multe despre acțiunile asociate regulilor

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.

Nu este posibil să setăm valoarea întoarsă de regulă într-o acțiune din mijlocul regulii, deoarece atribuirea către $$ 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.

Introducere în CMake

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:

  • -G pentru specificarea sistemului de build folosit (e.g. cmake -G “Eclipse CDT4 - Ninja” /path/to/source/tree)
  • -D pentru definirea unor variabile utilizate de fișierele de configurare
  • -LH pentru afișarea celor mai relevante variabile care pot fi definite pentru proiect

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.

Introducere în LCPL

Î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ă.

Structura unui program LCPL

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;

Tipuri de date și clase speciale

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

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

Clasa IO

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

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

Instrucțiuni

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.

Expresii

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 ')'.

Structura lexicală

LCPL conține următorii atomi lexicali:

  • întregi - șiruri nevide de cifre 0-9 care sunt 0 sau nu incep cu 0
  • identificatori - șiruri diferite de cuvintele cheie, conținând litere, cifre și '_'
  • \n - linie nouă
  • \r - carriage return
  • \t - tab

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.

Exerciții de laborator (13p)

În rezolvarea laboratorului folosiți arhiva de sarcini lab03_mini_lcpl_parser.zip

Conținutul mini-lcpl-parser

În directorul mini-lcpl-parser există două sub-directoare:

  • lcpl-AST
    • conține structurile de date ale arborelui sintactic, precum și codul care serializează nodurile din arbore. Pe parcursul exercițiului veți construi noduri din arborele sintactic folosind aceste structuri.
    • pentru a compila biblioteca lcpl-AST intrați in directorul lcpl-AST si executati urmatorii pasi:
      • mkdir build; cd build; cmake ..; make
    • în subdirectorul include/ sunt definițiile claselor care compun AST-ul
    • cui corespund aceste clase ? Gândiți-vă cum ați structurat gramatica în laboratorul trecut și de ce informații ați avea nevoie, informații pe care să le preluați în momentul executării unei acțiuni asociate unei reguli
    • hint: cum ați ales neterminalii în momentul în care ați gandit logica gramaticii ? Urmând acest fir, cum ar trebui să fie “traduși” acești neterminali în AST: ar trebui să apară în AST sau nu ?
  • lcpl-parser
    • conține un schelet al parserului
    • pentru a compila parserul intrați in directorul lcpl-parser și dați comanda make. În urma compilării rezultă executabilul lcpl-parser
    • tot in acest director veți găsi două foldere, unul cu teste pentru fiecare exercițiu exemple, precum și rezultatul de referință. Pentru a genera fișierul .ast, rulați ./lcpl-parser path_fisier
    • pentru ușurință puteți să folositi scriptul compare.sh, ce generează fișierul .ast și îl compară cu referința. (ex: ./compare.sh basic sau ./compare.sh all, pentru rularea tuturor testelor)
    • pe măsură ce preluați câte un exercițiu pe care trebuie să îl rezolvați, gândiți-vă la următorii pași:
      • tokenii pe care trebuie să îi prelucrez sunt generați de analizorul lexical ?
        • dacă da, continuați
        • dacă nu, modificați în consecință fișierul lcpl.l și/sau lcpl.y pentru a recunoaște noii tokeni și tipul lor
      • analizorul sintactic (gramatica) conține reguli conform cu ceea ce vreau să interpretez ?
        • dacă da, acțiunile asociate sunt cele pe care le urmăresc ? Sunt complete ?
        • dacă nu, găsiți unde se potrivesc noile reguli în cadrul gramaticii, conform sintaxei LCPL descrise în documentația laboratorului
        • puteți modifica regulile existente cum credeți de cuviință, sunt scrise pentru a vă ghida, dar nu toate sunt complete
      • în momentul scrierii acțiunii asociate unei reguli, luați în calcul următoarele:
        • ce reprezintă elementele regulii curente ? o expresie, o propoziție, o adunare, o grupare, o instanțiere ? după caz, conform limbajului de intrare
        • care va fi rezultatul expresiei curente și cum o evaluez ? Tipul asociat este recunoscut in cadrul analizorului ? Unde trebuie definit acest tip ?
        • cum arată un nod în cadrul AST-ului care va reprezenta regula/elementele regulii curente ? În cazul scheletului de laborator, căutați aceste clase in directorul lcpl-AST/include/, ele sunt deja implementate

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).

Observați legătura dintre clasele reprezentând nodurile AST-ului și elementele gramaticii definite în Bison.

Analizorul lexical

Va trebui să completați analizorul lexical astfel încât să recunoască și să paseze către analizorul sintactic următorii atomi lexicali:

  • cuvinte cheie: class, inherits, if, then, else, end, new, var
  • identificatori
  • constante întregi
  • operatorul logic <
  • operatorii binari * și -
  • operatorul de asignare =
  • operatorul de atribuire al tipului de retur din funcție →

Luați în calcul implicațiile faptului că '-' este atât operator, cât și parte a sintaxei de definire a unei metode

Țineți cont de faptul că o parte dintre ei sunt deja implementați. Trebuie doar să îi căutați pe cei care lipsesc și să adăugați codul corespunzător.

Analizorul sintactic

Pentru fiecare nod nou adaugat urmăriți modul de inițializare al acestora din folderul lcpl-AST/include.

Exercițiul 1 (3p)

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”.

Exercițiul 2 (3p)

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”.

Exercițiul 3 (4p)

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”.

Exercițiul bonus (4p)

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”.

cpl/labs/03.txt · Last modified: 2016/10/05 23:17 by bogdan.nitulescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0