This is an old revision of the document!


03. Bison Advanced

In 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 sa 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 in 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 in 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 (10p)

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

Pornind de la scheletul parserului pentru LCPL aflat in directorul mini-lcpl-parser, va trebui să completați acest parser astfel incât să poată parsa următorul cod LCPL:

# Factorial class
class Factorial 
  fact Int n -> Int :
    if n < 1 then
      1;
    else
      n * [fact n-1];
    end;
  end;
end;

# Main class
class Main inherits IO
  var
    Factorial f = new Factorial;
  end;
  
  main :
    [out [f.fact 10]];
  end;
end;

Conținutul mini-lcpl-parser

În directorul mini-lcpl-parser exisă 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 și dați comenzile cmake . urmată de make.
  • 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ă exemple simple.lcpl si factorial.lcpl, precum si fișierele .ast care corespund acestora. Pentru a genera fișierul simple.lcpl.ast, rulați ./lcpl-parser simple.lcpl.

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 -

Analizorul sintactic

Analizorul sintactic va trebui extins astfel încât sa poată parsa:

  • atribute inițializate
  • definiții de metode cu parametri formali
  • operatorii binari '-' (expresie aditivă) si '*' (expresie multiplicativă)
  • apeluri de metode cu argumente
  • apeluri de metode cu obiect pe care se apelează metoda (dispatch)
  • definiții de clase care moștenesc alte clase
  • operatorul new

Va trebui sa construiți urmatoarele noduri din arborele sintactic:

  • Class
  • Attribute
  • Method
  • FormalParam
  • Block
  • IntConstant
  • BinaryOperator
  • Dispatch
  • NewObject
cpl/labs/03.1445294115.txt.gz · Last modified: 2015/10/20 01:35 by cristina.ciocan
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