Ćwiczenie 1: Połączenie Flexa i Bisona.

To jest dalszy ciąg artykułu wprowadzającego i zakładam, że go czytałeś i masz pliki, które przy jego okazji utworzyłeś.

W tym poradniku zajmiemy się następującymi kwestiami:

  1. Wyrzucimy funkcję yylex() z pliku Bisona i zaimplementujemy ją w narzędziu Flex.
  2. Skompilujemy dwa pliki wygenerowane przez Flexa i Bisona w jeden program.
  3. Dodamy do naszego programu możliwość przetwarzania liczb nieujemnych oraz ignorowania spacji.

Stworzenie funkcji yylex() za pomocą Flexa.

Przypomnę, że funkcja yylex() do przykładowego parsera wyglądała tak:

int yylex() 
{
    int c;
    c = getchar();
    if (isdigit(c)) 
    {
        yylval = c - '0'; //przekaż wartość semantyczną
        return DIGIT; //przekaż typ tokenu
    }
    else if('\n' == c)
    {
        return 0; //koniec danych wejściowych
    }
    return c;
}

Możemy ją bardzo szybko przerobić na następujący plik Flexa:

%{
    #include<stdio.h>
    #include<math.h>
%}
 
%option noyywrap
 
ENDL \n
 
%%
 
[[:digit:]] {
    yylval = yytext[0] - '0';
    return DIGIT;
}
 
{ENDL} {
    return 0; //koniec danych wejściowych
}
 
. {
    return yytext[0];
}
 
%%

Ten fragment nie wymaga chyba komentarzy. Nazwijmy ten plik lexer.lex.

Oczywiście należy usunąć funkcję yylex() z pliku Bisona.

Kompilacja plików Flexa i Bisona w jeden program.

Jeśli teraz spojrzymy na nasze pliki, to zauważymy, że plik lexer.lex korzysta m.in. z tokenów i zmiennej yylval, które są generowane do pliku tutorial.tab.c, zatem lekser nie ma do nich dostępu i nie wie tak naprawdę, co to za wartości. Analogicznie, parser nie ma pojęcia, gdzie szukać funkcji yylex(). Mogłoby to stanowić potencjalny problem, jednak twórcy obu narzędzi przychodzą nam z pomocą. Zarówno w Flexie jak i Bisonie istnieje możliwość wygenerowania plików nagłówkowych, które będą zawierać deklaracje odpowiednich wartości. Jedyne co zatem pozostanie po wygenerowaniu tych nagłówków, to włączyć nagłówek z Bisona do pliku Flexa i włączyć plik nagłówkowy Flexa do Bisona.

W narzędziu flex do generowania nagłówka służy przełącznik —header-file=nazwapliku.h. Zatem instrukcja generująca odpowiednie pliki przyjmie postać:

flex -o lexer.c --header-file=lexer.h lexer.lex

Powyższa instrukcja wygeneruje nam pliki lexer.c i lexer.h z pliku lexer.lex.

W Bisonie można zrobić analogicznie, z tym, że trzeba użyć przełącznika -d, czyli:

bison -o parser.c -d tutorial.y

Powyższa linijka wygeneruje nam pliki parser.c i parser.h z pliku tutorial.y. W starszych wersjach bisone trzeba by było jeszcze dorzucić przełącznik —header-file=parser.h.

Najlepiej jeszcze nie wykonuj tych instrukcji, tylko najpierw dopisz włączanie odpowiednich plików do leksera i parsera.
W pliku lexer.lex powinna się znaleźć na początku linijka:

#include "parser.h"

…natomiast w pliku tutorial.y (też na początku):

#include "lexer.h"

Zbudowanie binarki składa się z następujących kroków:

flex -o lexer.c --header-file=lexer.h lexer.lex
bison -o parser.c -d tutorial.y
gcc -c parser.c lexer.h
gcc -c lexer.c parser.h
gcc -o program lexer.o parser.o

Oczywiście najwygodniej jest utworzyć sobie makefile'a, który za każdym razem będzie to robił automatycznie, ale nie będę się tym tutaj zajmować.

Teraz można odpalić i wypróbować binarkę - powinna działać tak samo, jak program stworzony przy okazji wprowadzenia.

Dodanie do programu obsługi liczb nieujemnych oraz ignorowania spacji.

Dodanie tych możliwości sprowadza się do kilku drobnych zmian w lekserze (zauważ, że nie tykamy parsera), a mianowicie zastąpienie bloku kodu:

[[:digit:]] {
    yylval = yytext[0] - '0';
    return DIGIT;
}

następującym:

[[:digit:]]+ {
    yylval = atoi(yytext);
    return DIGIT;
}

…oraz dodanie następującej reguły:
[[:blank:]] { /*EMPTY BODY*/ }

Oczywiście trzeba załączyć także plik nagłówkowy math.h oraz skorzystać z opcji -lm przy ostatnim kroku tworzenia programu (gcc -o program lexer.o parser.o -lm). Tak na marginesie - nazwa DIGIT dla typu tokenu jest teraz mało odpowiednia, bo przetwarzamy całe liczby, a nie tylko cyfry. Można ją zmienić, żeby lepiej oddawała rzeczywistość (np. na NUM).

Pozostaje teraz ponownie przeprowadzić procedurę budowania (albo użyć makefile'a) i przetestować binarkę.

Przykładowa sesja z programem może wyglądać tak:

astral@astral-laptop:~/Programming/Bison$ ./program 
123123  +      34
123123 + 34 = 123157
astral@astral-laptop:~/Programming/Bison$

Dodatek: Listingi końcowe poszczególnych plików.

Plik tutorial.y:

%{
    #include"lexer.h"
    int yyerror(char const* s);
 
%}
 
%token NUM
 
%%
 
expr : NUM;
 
expr : expr '+' NUM { 
    $$ = $1 + $3; 
    printf("%d + %d = %d\n", $1, $3, $$); 
};
 
%%
 
int yyerror(char const* s)
{
    printf("%s\n", s);
    return 1;
}
 
int main(void)
{
    yyparse();
    return 0;
}

Plik lexer.lex:

%{
    #include "parser.h"
    #include<stdio.h>
    #include<math.h>
%}
 
%option noyywrap
 
ENDL \n
 
%%
 
[[:digit:]]+ {
    yylval = atoi(yytext);
    return NUM;
}
 
[[:blank:]] { /*EMPTY BODY*/ }
 
{ENDL} {
    return 0; //end of output
}
 
. {
    return yytext[0];
}
 
%%

Procedura budowania binarki:

flex -o lexer.c --header-file=lexer.h lexer.lex
bison -o parser.c -d tutorial.y
gcc -c parser.c lexer.h
gcc -c lexer.c parser.h
gcc -o program lexer.o parser.o -lm

astralastral

+Literatura:
instrukcja do Bisona
instrukcja do Flexa

O ile nie zaznaczono inaczej, treść tej strony objęta jest licencją Creative Commons Attribution-Share Alike 2.5 License.