Table of Contents

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:

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:

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:

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:

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:

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