This is an old revision of the document!
Bison
este un generator de analizoare sintactice. Acesta primește la intrare gramatica unui limbaj pe care o translatează într-un parser pentru acel limbaj. Gramaticile pentru bison
sunt descrise folosind o variantă a BNF (Backus Naur Form). Gramaticile BNF pot fi folosite pentru limbaje independente de context. Parserele generate cu Bison
pot folosi una din urmatoarele metode de parsare:
Majoritatea parserelor folosesc LALR(1), care are mai putine capabilitati, insa este semnificativ mai rapid si mai usor de folosit decat GLR. Si noi, vom genera tot parsere LALR.
Bison
este varianta GNU a yacc
(Yet Another Compiler-Compiler).
Fișierul de intrare, ce conține gramatica, are extensia .y
(este o convenție). Implicit, programul C generat are același nume ca fișierul de specificație, însă extensia .y
se schimbă în .tab.c
.
În cadrul acestui laborator vom folosi pachetul bison
standard oferit de distribuție. Pentru mașina virtuală CPL pusă la dispoziție versiunea pachetului bison
este 3.0.2
.
sudo apt-get install bison
bison [OPTIONS] file
Comanda dispune de o varietate de parametri și opțiuni (man bison) dintre care amintim:
yydebug
trebuie să fie setată pe o valoare nenulă, implicit având valoarea 0.output
în loc de .tab.c
Fișierul de specificații este format din patru secțiuni:
%{ // sau %code { C declarations %} Bison declarations %% grammar rules %% C code
Utilizatorul trebuie să definească cel puțin trei funcții C:
yyparse
yyparse
pentru obținerea atomilor lexicaliyyparse
pentru afișarea mesajelor de eroareSecțiunea de declarații C conține
Codul C este inclus în fișierul .c generat așa cum a fost scris în această secțiune, fără modificări.
Regulile unei gramatici conțin terminali și neterminali. În gramatica acceptată de bison
vom denumi terminalii tokeni. Ei sunt rezultatul analizei lexicale (În cazul de față făcută cu ajutorul flex
).
În această secțiune se vor defini:
%token <tip> nume1 nume2
%start <tip> nume1 nume2
Aceștia vor fi declarați numai dacă se vor defini mai multe tipuri pentru yylval, cum vom vedea mai jos.
%type <tip> nume1 nume2 ...
int
. În general, este nevoie să returnăm mai multe tipuri de valori, nu doar int. Tipurile de valori ce pot fi returnate sunt specificate tot în această secțiune într-un union
. Odată ce am definit mai multe tipuri pentru tokeni, avem nevoie de tipuri și pentru neterminali.%union { int int_value; char* str_value; }
În exemplu am pus tipuri de bază, dar pot fi și tipuri definite de utilizator. În acest caz, tipurile pentru tokeni și pentru neterminali pot fi: int_value și str_value.
%token <int_value> INTEGER %token <str_value> STR %type <int_value> exp
Dacă este suficient să întroarcem valori de tip int, și nu definim un union de tipuri, atunci <token_type>
este omis în declararea tokenilor.
O listă completă cu simbolii bison
găsiți aici
O producție a gramaticii de forma:
A -> B1B2...Bn
în bison
va fi scrisă astfel:
A : B1B2...Bn ;
unde A, rezultatul, este un neterminal, iar B1..Bn sunt o succesiune de terminali și neterminali.
expr : expr ‘+’ expr ;
Dacă există mai multe reguli pentru același rezultat, fie pot fi scrise separat, fie pot fi separate de '|', ca mai jos:
expr : NUM { $$ = $1;} | expr ‘+’ expr { $$ = $1 + $3;} ;
Ca și regulile lexicale, regulile gramaticii pot să aibă o acțiune asociată, ce reprezintă cod C. Sintaxa este
{C statements}
$$
reprezinta rezultatul, adica valoarea ce va fi atribuită neterminaului de la stânga regulii, cel la care se va reduce regula.$n
este al n-lea termen din regula sintactica.Acțiunile definite în mijlocul unei reguli se folosesc numai în anumite situații, pot folosi doar simbolii anteriori acesteia (fiindcă se execută înainte ca simbolii următori regulii să fie parsați) și sunt o sursă de conflicte. Vom reveni asupra acestora în laboratorul urmator.
Secțiunea de cod C trebuie să conțină:
yyerror
cu prototipul:void yyerror(char *s)
main
care conține un apel către yyparse()
Vom vedea acum cum comunică analizorul lexical (flex) cu analizorul sintactic (bison).
Bison
generează un parser, din nume_gram.y
, în fișierul nume_gram.tab.c
și un fișier header nume_gram.tab.h
. În fișierul header fiecare token are asociat un numar. Fișierul de intrare pentru lex (.l) include fișierul header (cu extensia .tab.h
) și folosește numerele atribuite tokenilor în acest fisier.
Intrarea pentru bison
este un șir de tokeni furnizat de flex prin funcția yylex().
Numerele asociate tokenilor:
[-+] return *yytext;
va returna valoarea ASCII a caracterului ‘-’ (45), respectiv valoarea ASCII a caracterului ‘+’ (43)
error
este rezervat pentru tratarea erorilor
Să presupunem că avem o gramatică care definește token-ul INTEGER și acesta a primit valoarea 258 în fișierul header nume_gram.tab.h. Fiecare apariție a instrucțiunii “return INTEGER” din fișierul .l va fi înlocuită, în acest caz cu “return 258”. Pentru a obține tokenii, bison apelează funcția yylex. Altfel spus, fișierul cu extensia .tab.h atribuie fiecărui token/terminal un identificator unic de tip int. Cum acest header este inclus în fisierul *.l, numele tokenilor sunt înlocuite cu identificatorul unic.
Dacă un token are asociată o valoare, aceasta poate fi transmisă către parser prin variabila yylval
.
(în cazul nostru un int)
Conflictele sunt rezultatul unei gramatici ambigue. Conflictele pot fi fie shift/reduce, fie reduce/reduce
Exemplu de conflict shift/reduce datorat ambiguitatii dangling else
:
Cand token-ul else
este citit si devine token-ul lookahead, ceea ce a fost deja citit se potriveste pe prima regula si ar putea fi redus. Dar, este de asemenea legal sa shiftam else-ul, pentru ca sirul de tokeni de la intrare s-ar putea potrivi pe a doua regula.
Deoarece parser-ul prefera sa shifteze, else-ul va fi atasat if-ului cel mai imbricat.
Daca parser-ul ar alege sa reduca, atunci cand poate, si nu sa shifteze, else-ul va fi atasat if-ului exterior(primul).
if_stmt: "if" expr "then" stmt | "if" expr "then" stmt "else" stmt ;
Cu aceasta gramatica, secventa de intrare if (exp1) if (exp2) stmt1 else stmt2
poate fi parsata in 2 moduri diferite:
Cazul 1: if (e1) { if (e2) s1 else s2 } Cazul 2: if (e1) { if (e2) s1 } else s2
Acesta este un asa zis conflict shift/reduce legitim. Exista cazuri in care gramatica genereaza astfel de conflicte pentru că a fost scrisa ambiguu, desi se putea scrie si intr-o forma neambigua. In aceste cazuri se recomanda rescriere regulilor cu conflicte.
expr : expr ’+’ expr | expr ’*’ expr | ’-’ expr | ID | NUMBER ;
Gramatica expresiilor genereaza conflicte cand uitam sa implementam asociativitatea si precedenta tokenilor. Sunt 2 moduri de a specifica precedenta si asociativitatea pentru o gramatica: implicit si explicit. Cand o specificam implicit trebuie sa introducem un simbol neterminal pentru fiecare nivel de precedenta. Iata mai jos solutia implicita:
expr: expr '+' factor | factor ; factor: factor '*' term | term ; term: '-' term | ID | NUMBER ; ;
Gramatica devine mai stufoasa, dar neambigua. Metoda explicita presupune folosirea explicita a regulilor de precedenta suportate de Bison.
Asociativitatea si precedenta pot fi specificate in urmatorul mod:
%left
, %right
, %nonassoc
%left
%prec
. Acesta schimba precedenta unei reguli la precedenta tokenului urmator.%left ’+’ ’-’ %left ’*’ ’/’ ... expr : expr ’+’ expr | expr ’*’ expr | ’-’ expr %prec ’*’ | ID ;
Exercitiu: Incercati sa rezolvati conflictul shift/reduce pentru dangling else
folosind reguli de precedenta.
bison –d ex1.y flex ex1.l cc lex.yy.c ex1.tab.c –o ex1 ./ex1 input_file
În rezolvarea laboratorului folosiți arhiva de sarcini lab02_simple_ops.zip
Intrați în directorul 1-simple-ops
. calc.y
si calc.l
sunt analizorul lexical și cel sintactic pentru un calculator care poate evalua expresii aritmetice cu operanzi întregi și operatori '+' si '*'. Expresiile sunt citite dintr-un fișier de intrare (de exemplu add-mul-test
), fiecare expresie se încheie cu ';'. Generați cu comanda make
fișierul executabil și rulați testul add-mul-test
.
Modificați apoi calculatorul astfel încât să accepte și operatorii '-' și '/'. Pentru a verifica implementarea, rulați testul div-sub-test
.
Extindeți exercițul anterior astfel încât calculatorul să accepte instrucțiuni de atribuire și variabile și să poată evalua expresii care conțin variabile cărora le-a fost atribuită o valoare. Pentru a implementa această extensie, veți avea nevoie de o tabelă de simboli cu ajutorul căreia să țineți minte variabilele cărora li s-a atribuit o valoare și din care să citiți valoarea curentă a unei variabile în cazul în care variabila este operand al unei expresii. Tabela de simboli o puteți implementa folosind std::map<char *, int>
.