Analiza syntaktyczna cz. 1

Wprowadzenie

Pyacc jest narzędziem, które pozwala narzucić i kontrolować strukturę wejścia dla potrzeb różnego rodzaju programów komputerowych. Użytkownik pyacc-a przygotowuje specyfikację określającą jak powinno wyglądać wyjście. Sercem specyfikacji są reguły opisujące strukturę wejścia.

Pyacc generuje funkcję analizatora składniowego, która wywołuje dostarczoną przez użytkownika funkcję analizatora leksykalnego do obsługi wejścia na niższym poziomie. Analizator leksykalny znajduje w strumieniu wejściowym podstawowe jednostki leksykalne zwane tokenami. Tokeny są dopasowywane do reguł opisujących strukturę wejścia, zwanych regułami gramatycznymi. Gdy wejście zostanie dopasowane, wykonywany jest kod dostarczony przez użytkownika dla danej reguły gramatycznej, zwany akcją.

Pyacc jest generatorem analizatorów składniowych (parserów) typu LALR.

Za pomocą pyacc-a można stworzyć parser w sposób pokazany na rysunku:

Uwaga: Wygenerowany przez pyacc-a parser wymaga podania kodu skanera. Najczęściej kod skanera jest generowany przy pomocy plex-a i dołączany do kodu parsera na etapie kompilacji. Kod skanera może być też utworzony przez programistę i umieszczony bezpośrednio w specyfikacji pyacc-a.

Specyfikacja

Specyfikacja dla pyacc-a ma następujący układ:

      {deklaracje}
      %%
      {reguły}
      %%
      {procedury}

Deklaracje

W sekcji deklaracje pomiędzy znakami %{ i %} umieszczany jest kod w języku Pascal, który ma być wstawiony bezpośrednio na początku generowanego programu, np.

      %{
      uses YaccLib, LexLib;
      %}

Następnie znajdują się deklaracje symboli otrzymywanych od analizatora leksykalnego, zwanych tokenami. Nazwy używanych symboli leksykalnych są deklarowane w następujący sposób:

      %token NAZWA1 
      %token NAZWA2 
      %token NAZWA3

i zwyczajowo pisane sa duzymi literami.

Dla każdego symbolu terminalnego lub nietermianlnego może być osobno określony typ. Powiązanie typu symbolu jest powiązane delkaracją symbolu i wygląda następująco:

	%token <Real> NAZWA_TOKENU

w znaki < oraz > ujęta jest nazwa typu związanego z symbolem (w powyższym przykładzie jest to typ Real).

W przypadku symboli nieterminalnych powiązanie ich z typami przyjmuje postać:

	%type <Real> nieterminal1 
	%type <Char> nieterminal2

Reguły

W tej sekcji umieszczamy reguły gramatyczne.

Ogólna postać reguły jest następująca:

      <lewa strona>  : <alternatywa 1>   {akcja 1}
                     | <alternatywa 2>   {akcja 2}
                     ...
                     | <alternatywa n>   {akcja n}
                     ;

Przykładowo produkcje, które zapisywaliśmy jako:

      E --> E + T | T
      T --> 0 | ... | 9

w pyacc-u można zapisać jako:

      wyr  :  wyr '+' term 
           |  term
           ;
      term : CYFRA
           ;

Symbole terminalne mogą być dwojakiego rodzaju:

  1. identyfikator zadeklarowany wcześniej jako %token TERMINAL_NAME; zwyczajowo identyfikatory terminali pisane są dużymi literami,
  2. pojedynczy znak w apostrofach (literał), np. '+'.

Nazwa niezadeklarowana jako token, traktowna jest jako symbol nieterminalny. Każdy z symboli nieterminalnych musi przynajmniej raz pojawić się po lewej stronie reguły.

Domyślnie zakłada się, że symbolem początkowym gramatyki jest lewa strona pierwszej reguły. (Symbol początkowy gramatyki można zdefiniować jawnie w sekcji deklaracji w postaci %start symbol).

Z każdą regułą skojarzona może być akcja semantyczna. Akcja jest sekwencją instrukcji w Pascalu zawartą wewnątrz znaków nawiasów { i }. Akcja jest wykonywana w momencie, gdy parser rozpozna na wejściu strukturę, która została określona w regule. Akcja można definiowana także w środku definicji reguł.

Akcje mogą zwracać wartości i korzystać z wartości zwróconych przez inne akcje. Do tego celu wykorzystuje się zmienne $$, $1, $2, $3...

Zmienna $$ przechowuje wartość zwracaną przez akcję. Zaś zmienne $1, $2, $3... przechowują wartości odpowiadające kolejnym składnikom reguły, np.

      wyr : wyr '+' term { $$ = $1 + $3 }
      /*
        $$ oznacza wartość zwracaną przez akcję,
        $1 oznacza wartość skojarzoną z symbolem wyr (po prawej stronie produkcji),
        $3 oznacza wartość skojarzoną z symbolem term,
        
        podstawienie $$ = $1 + $3 spowoduje, że akcja zwróci wartość będącą sumą 
        wartości skojarzonych ze składnikami 1 i 3
      */

Jeżeli nie zdefiniowano inaczej domyślnie wartością reguły staje się wartość pierwszego składnika ($1) tj.

      A : B {$$ = $1};     oznacza to samo co     A : B ;
Domyślnym typem wartości semantycznych jest Integer. Można zmienić domyślne ustawienia wpisując sekcji deklaracji odpowiednio:
	%{
	type YYSType = Char;
	%}

Procedury pomocnicze

Użytkownik musi dostarczyć pyacc-owi analizator leksykalny odpowiedzialny za bezpośrednie kontrolowanie wejścia i dostarczanie symboli terminalnych do parsera. Taka funkcję można wygenerować przy pomocy plex-a lub napisac wlasnoręcznie. Analizator leksykalny musi być funkcją zwracającą Integer i mieć nazwę yylex.

	function yylex : Integer;

Skaner ma za zadanie zwracać numer symbolu przeczytanego z wejścia. Wartość atrybutu dla danego symbolu leksykalnego dostarczana jest parserowi poprzez predefiniowaną zmienną yylval.

Wewnątrz sekcji procedury pomocnicze często umieszczane są procedury obsługi błędów.

Uwaga: Komantarze mogą być umieszczone w dowolnym miejscu specyfikacji i są zawarte między znakami /* i */, np. /* komentarz */.

Uwaga:Składnia pliku generatora pyacc jest opisana szczegółowo wraz z przykładami w dokumentacji programu pyacc.

Prosty przykład

Poniżej znajduje się kod parsera, który oblicza wartość wyrażeń arytmetycznych złożonych z liczb całkowitych i znaków dodawania.

/* add.y
Obliczanie sumy.
Parser czyta z wejscia wyrazenia arytmetyczne zlozone z liczb calkowite i znakow '+', 
na wyjsciu wypisuje wynik dodawania. 

*/

%{
uses YaccLib, LexLib;
%}

%token NUM

%%

input	:  
	|  input '\n'
	|  input  expr '\n' { writeln($2); }
	;

expr	: expr '+' NUM	{ $$ := $1 + $3;}
	| NUM
	;
%%

{$I addlex.pas}

begin
  yyparse;
end.

Źródła:  add.y

%{
(* addlex.l
   Skaner wspolpracujacy z parserem add.y *)
%}

%%

[0-9]	begin 
	   val(yytext, yylval); 
           return(NUM);
	end;

.       |
\n      returnc(yytext[1]);

Źródła:  addlex.l

Przygotowujemy plik ze specyfikacją pyacc-a, np. example.y. Następnie przekształcamy specyfikację na program pascalowy example.pas przy pomocy polecenia:

pyacc example.y

Jeżeli potrzeba, przygotowujemy plik ze specyfikacją plex-a, np. examplelex.l i generujemy kod skanera examplelex.pas poleceniem:

plex examplelex.l

Kompilujemy wygenerowany kod parsera poleceniem:

ppc386 -Sg example.pas

Otrzymujemy program wynikowy example. Możemy go uruchomić przykładowo podając na wejście plik test_parser:

./example <test_parser

Inne przykłady

Poniżej umieszczony jest przykład specyfikacji pyacc-a dla parsera uproszczonej gramatyki wyrażeń arytmetycznych. Parser czyta z wejścia wyrażenia arytmetyczne złożone z liczb, operatorów + - * / oraz nawiasów i wypisuje na wyjście wyniki obliczeń.

/* expr.y 
Parsera uproszczonej gramatyki wyrażeń arytmetycznych. Czyta z wejscia 
wyrazenia arytmetyczne zlozone z liczb, operatorów + - * / oraz nawiasów 
i wypisuje na wyjscie wyniki obliczeń.
*/
%{
uses YaccLib, LexLib;
%}

%token NUM       

%%

input	: /* empty */
	| input '\n'		 { yyaccept; }
	| input expr '\n'	 { writeln($2); }
	;

expr    :  expr '+' term	 { $$ := $1 + $3; }
	|  expr '-' term	 { $$ := $1 - $3; }
	|  term			 
	;

term	:  term '*' fact	 { $$ := $1 * $3; }
	|  term '/' fact	 { $$ := $1 div $3; }
	|  fact
	;

fact	: '(' expr ')'		 { $$ := $2; }
	|  NUM                   
	;

%%

{$I exprlex.pas}

begin
  yyparse;
end.

Źródła:  expr.y

%{
  (* exprlex.l
     Skaner wspolpracujacy z parserem expr.y. *)
%}
%%
[0-9]+				begin
				  val(yytext, yylval);
				  return(NUM)
				end;

" "             		;
.				|
\n				returnc(yytext[1]);

Źródła:  exprlex.l

Poniżej znajduje się kod najprostszego parsera ilustrujący przedefiniowanie domyślnego typu tokenu z Integer na Char.

/* char.y
Najprostszy parser ilustrujacy przedefiniowanie domyslnego typu tokenu z Integer na Char
*/

%{
uses YaccLib, LexLib;

type YYSType = Char;

%}

%token T_CHAR

%%

input	: 
	| input '\n' 		{ yyaccept; }
	| input znaki '\n' 	
	;

znaki	:  znaki T_CHAR  { writeln('Znaleziono: ', $1); }
	|  T_CHAR  	{ writeln('Znaleziono: ', $1); }
	;

%%

{$I charlex.pas}

begin
   yyparse;
end.

Źródła:  char.y

%{
  (* charlex.l
     skaner wspolpracujacy z parserem char.y *)
%}
%%
[a-zA-Z]	begin
		  yylval := yytext[1];	
		  return(T_CHAR);
		end;
.		|
\n		returnc(yytext[1]);

Źródła:  charlex.l

Ćwiczenia

Ćwiczenie 1

Przeanalizować, skompilować i uruchmić przykłady przedstawione na tej stronie.

Ćwiczenie 2

Napisać Makefile, który ułatwia tworzenie parserów z wykorzystaniem pyacc-a i plex-a.

Ćwiczenie 3

Proszę napisać parser, który rozpoznaje poprawne wywołania kompilatora gcc w postaci:

gcc nazwa_pliku1.c [nazwa_pliku2.c ...]

Zakładamy, że nazwa pliku składa się z liter i cyfr.

Ćwiczenie 4

Rozszerzyc parser z poprzedniego zadania o mozliwosc rozpoznawania opcji:

-o pliku_wyn

Przykłady:

WywołanieRezultat
gccerr
gcc plik.cok
gcc plik1.c plik2.cok
gcc -o plik_wyn plik.cok
gcc -o plik_wyn plik1.c plik2.cok