Flex+Bison: lekseemi asukoha leidmine

Süntaksi- ja semantiliste vigade leidmisel on tarvis kasutajale teatada vea asukoht. Lekseemi asukoha salvestamiseks on Bison-is olemas andmetüüp YYLTYPE mis on määratud neljaväljalise struktuurina:

typedef struct YYLTYPE
{ int first_line; int first_column; int last_line; int last_column; } YYLTYPE;

Bison-is on ka määratud muutuja yylloc, mille tüüp on YYLTYPE, kuid Flex ei tunne kumbagi ja väärtused yylloc väljadele peab andma Flex - hiljem on see info kadunud.

Kui skanner on vormistatud omaette failis, peab Flex-ile tegema kättesaadavaks YYLTYPE definitsiooni. Selleks lastakse see Bison-il kirjutada eraldi header-faili, mis siis include-lausega Flex-is kasutusele võetakse. Header-faili võib moodustada kahel viisil: kas Bison-i käivitamisel võtme -d kasutamisega või Bison-i teksti deklaratsioonide osasse deklaratsiooni %defines lisamisega. Header-faili nime moodustab Bison sama eeskirja järgi nagu C-programmifailigi, lisades sisendfaili nimele lõppu .tab, kuid failinime laiendiks tuleb .h. Seega kui Bison-i sisendfail on näiteks calc.y, tuleb header-faili nimeks calc.tab.h ja C-programmi nimeks - calc.tab.c. Flex-i sisendfaili C-deklaratsioonide ossa tuleb lisada header-faili lisamine ja muutuja yylloc deklaratsioon:

#include "calc_loc.tab.h"
struct YYLTYPE yylloc;

Muutuja yylloc väljade algväärtustamise võib teha näiteks peaprogrammi alguses:

int main (void)
{ yylloc.first_line = yylloc.last_line = 1; yylloc.first_column = yylloc.last_column = 0; return yyparse (); }

Flex-i programmis peavad olema ka tegevused, mis tagavad, et muutuja yylloc väljade väärtus oleks kogu aeg õige; näiteks täisarvu leidmisel peab suurendama reas positsiooni näitavaid muutujaid yylloc.first_column, yyloc.last_column, kasutades arvu pikkust näitavat globaalmuutujat yyleng:

{number}+     {yylloc.first_column = yylloc.last_column+1; yylloc.last_column +=yyleng; return(ARV); }

Leksikavea korral (lubamatu sümbol) teatab Flex veast, kuid ei saada seda lubamatut sümbolit Bison-ile edasi ("neelab alla").

Süntaksivea korral peaks Bison minimaalse vahelejätmisega tööd jätkama. Töö jätkamiseks on Bison-is makro yyerrok, kuid et Bison suudaks uuesti leida sisendis pidepunkti, peavad Bison-i sisendis olema selleks reeglid, kus vigane kontruktsioon on tähistatud Bison-i reserveeritud sõnaga (Bison-i poolt defineeritud mitteterminaliga) error. Kui analüüsitav sisend peab näiteks realiseerima kalkulaatori, kus igal real on mingi aritmeetiline avaldis, siis on loomulik hüljata kogu vigane rida:

rida   : '\n' /* tyhi rida */ | exp '\n' { printf ("Rida %d.%d..%d: %f\n", @1.first_line, @1.first_column,@1.last_column, $1); } | error '\n' { printf("Viga, rida: %d.%d..%d\n",@1.first_line, @1.first_column,@1.last_column ); yyerrok;} ;

Mitteterminali rida kolmas alternatiiv ütleb, et vea korral reas peab liikuma kuni rea lõpuni (kuni leitakse reavahetuse sümbol '\n') ja jätkama - käsk yyerrok määrab, et järgmiselt realt tööd jätkatakse.

Mõnikord näib otstarbekas lokaliseerida viga kitsamas piirkonnas. Kui avaldises näiteks võib kasutada sulge, siis tekib kiusatus deklareerida ka sulgude vahel olev vigane osa selliseks, mille järel peaks tööd jätkama, s.t. kasutada mitteterminali avaldis kirjeldamisel reegleid

avaldis:    ...
   | '(' avaldis ')' { $$ = $2; }
   | '('error')' { printf("Viga, rida: %d.%d..%d\n",@1.first_line, @1.first_column,@1.last_column );
     yyerrok;}

Kui selline defineerimine toob kaasa nn. sekundaarsed vead. Kui transleeritavas tekstis on näiteks rida

2+(3*4)

siis jäetakse alamavaldis (3*4) ära, kuid tuemuseks on ikkagi vigane avaldis 2+ ja tuleb uus veateade. Kui programmis on ka näiteks muutujate deklaratsioonid, siis põhjustab vigane deklaratsioon hiljem (semantilise analüüsi ajal) veateate kõikjal, kus seda (vea tõttu deklareerimata jäänud) muutajat kasutatakse - kuigi need kohad programmis võivad olla täiesti korrektsed.

Mõnda sellist uut viga ei suuda Bison isegi avastada. Kui avaldise reeglites on näiteks ka reegel unaarse miinuse jaoks:

avaldis:    -avaldis;

siis ülalesitatud sulgudes oleva vigase alamavaldise hülgamise reegli korral teatab Bison vigase avaldise (3*+4)-2 transleerimisel algul veast, kuid väljastab siiski tulemuse: -2 (vigase).

Järgnevas on näitena lekseemide asukohta ja leksika- ja süntaksivigu teatava kalkulaatori Flexi sisendfail calc.l:

%option noyywrap
%{
#include "calc.tab.h"
struct YYLTYPE yylloc;
%}

number [0-9]
tyhikud [' '|\t]
%%
"+"   {++yylloc.first_column; ++yylloc.last_column ; return('+');}
"-"   {++yylloc.first_column; ++yylloc.last_column ; return('-');}
"^"   {++yylloc.first_column; ++yylloc.last_column ; return('^');}
"*"   {++yylloc.first_column; ++yylloc.last_column ; return('*');}
"/"   {++yylloc.first_column; ++yylloc.last_column ; return('/');}
"("   {++yylloc.first_column; ++yylloc.last_column ;return('(');}
")"   {++yylloc.first_column; ++yylloc.last_column ;return(')');}
"\n"  { /*rea lõpp - algab uus rida */++yylloc.first_line;++yylloc.last_line;
       yylloc.first_column = yylloc.last_column = 0; return('\n');}
{number}+  {yylloc.first_column = yylloc.last_column+1; yylloc.last_column +=yyleng; return(ARV); }
{tyhikud}+ { yylloc.first_column =yylloc.last_column+1; yylloc.last_column +=yyleng ;/* tühikud ja tabulatsiooonimärgid real kustutatakse */}
.     {yylloc.first_column =yylloc.last_column+1; yylloc.last_column +=yyleng;
      printf("Lubamatu symbol: %s\n",yytext);};
%%

Bison-i sisendfailis on vigase rea ja vigase sulgudesse võetud avaldise leidmisel määratud käsuga yyerrok, et töö peab jätkuma.

Bison-i sisendis on kirjeldatud ka aritmeetiliste operatsioonide assotsiatiivsus, s.t. kuidas peaks arvutama näiteks avaldisi 2-2-2 ja 2^3^2 - kas paremalt vasakule või vasakult paremale? Operatsiooni '-' assotsiatiivsus on vasakult, seega arvutatakse 2-2-2 = (2-2)-2 = -2, kuid operatsiooni '^' assotsiatiivsus on paremalt ja 2^3^2 arvutatakse 2^3^2 = 2^(3^2) = 512. Assotsiatiivsuste määramisel on oluline ka määramiste järjekord: operatsioonide '*' '/' assotsiatiivsus on määratud pärast operatsioonide '+' '-' assotsiatiivsust, seega on operatsioonide '*' '/' prioriteet kõrgem kui operatsioonide '+' '-' prioritet, s.t. avaldise 2 + 3*4 väärtus tuleb 2 + (3 * 4) = 14, mitte (2 + 3) * 4 = 20.

Järgneb Bison-i sisendfail calc.y:

%{
#define YYSTYPE double
#include <math.h>
#include "lex.yy.c"
#include <stdio.h>
void yyerror (char const *);
%}

/* Bison-i deklaratsioonid */
%token ARV
%defines
%left '-' '+'
%left '*' '/'
%left NEG
%right '^'

%% /* Järgneb grammatika */
sisend : /* empty */
| sisend rida
;

rida : '\n'
     | avaldis '\n' { printf ("Rida %d.%d..%d: %f\n", @1.first_line, @1.first_column,@1.last_column, $1); }
     | error '\n' { printf("Viga, rida: %d.%d..%d\n",@1.first_line, @1.first_column,@1.last_column ); yyerrok;}
     ;

avaldis : ARV { $$ = atof(yytext); }
     | avaldis '+' avaldis { $$ = $1 + $3; }
     | avaldis '-' avaldis { $$ = $1 - $3; }
     | avaldis '*' avaldis { $$ = $1 * $3; }
     | avaldis '/' avaldis   { if ($3) $$ = $1 / $3;
     else
     { $$ = 1; fprintf (stderr, "%d.%d-%d.%d: jagamine nulliga! ",
     @3.first_line, @3.first_column, @3.last_line, @3.last_column);
     } }
     | '-' avaldis { $$ = -$2; }
     | avaldis '^' avaldis { $$ = pow ($1, $3); }
     | '(' avaldis ')' { $$ = $2; }
     | '('error')' { printf("Viga, rida: %d.%d..%d\n",@1.first_line, @1.first_column,@1.last_column );
    yyerrok;}
;

%%
void
yyerror (char const *s)
{
printf ( "Viga: %s- %s\n", s,yytext);
}

/* Bison-i käivitamisel initsialiseeritakse yylloc ! */
int main (void)
{yylloc.first_line = yylloc.last_line = 1;
yylloc.first_column = yylloc.last_column = 0;
return yyparse ();
}

Katsetame pärast gcc-ga transleerimist saadud kalkulaatorit näiteks sellise sisendiga:

2+ (3*(4-1))
2+..3*4
2+4/4a
8*4/4
4/(2-2)
4/
2^3^2

Tulemus tuleb selline (kuna nulliga jagamisel ilmenud viga saadekatse väljundisse stderr, väljastatakse vastav veateade alles pärast kogu töö lõppemist:

Rida 1.1..12: 11.000000
Lubamatud symbolid: .
Lubamatud symbolid: .
Rida 2.1..7: 14.000000
Lubamatud symbolid: a
Rida 3.1..5: 3.000000
Rida 4.1..5: 8.000000
Rida 5.1..7: 1.000000
Viga: syntax error-

Viga, rida: 6.1..2
Rida 7.1..5: 512.000000
5.3-5.7: jagamine nulliga !


Ülesandeid:
1. Ylesande tekst

Küsimused, probleemid: ©2004 Jaak Henno