Avanti Indietro Indice

6. Come lavorano internamente Lex e YACC

Nel file per YACC, siete voi a scrivere la vostra funzione main() che ad un certo punto chiama yyparse(). La funzione yyparse() viene creata automaticamente da YACC, e finisce in y.tab.c.

yyparse() legge un flusso di coppie simbolo/valore provenienti da yylex() che è necessario fornire. Si può scrivere questa funzione per conto proprio o lasciare questo compito a Lex. Nei nostri esempi si è scelto di lasciare questo compito a Lex.

La yylex() come scritta da Lex legge i caratteri da un puntatore a file FILE * chiamato yyin. Se non viene specificato yyin Lex usa lo standard input. L'output va in yyout, e se non specificato finisce sullo stdout. Si può anche modificare yyin nella funzione yywrap() che viene chiamata a fine file. Questa funzione permette di aprire un altro file, e continuare nell'analisi.

In questo caso va fatto in modo che restituisca 0. Nel caso si volesse far finire l'analisi dopo questo file gli si faccia restituire 1.

Ogni chiamata a yylex() restituisce un valore intero che rappresenta un tipo di categoria. Questo dice a YACC che genere di categoria ha letto. Il simbolo può eventualmente avere un valore, che dovrà essere messo nella variabile yylval.

Per default yylval è di tipo int ma si può cambiarlo dal file per YACC ridefinendo YYSTYPE con #define.

È necessario che l'Analizzatore lessicale (Lexer) sia in grado di accedere a yylval. Per questo la si deve dichiarare nell'ambito dell'Analizzatore lessicale (Lexer) come una variabile extern. Lo YACC originale tralascia di fare questo per voi, per cui si dovrebbe aggiungere il seguente codice al proprio Analizzatore lessicale (Lexer), giusto sotto #include <y.tab.h>:

extern YYSTYPE yylval;

Bison, che è utilizzato di questi tempi dalla maggioranza della gente, lo fa automaticamente per l'utilizzatore.

6.1 Valori di categoria (Token values)

Come detto precedentemente yylex() deve restituire quale tipo di categoria incontra e mettere il suo valore in yylval. Quando queste categorie vengono definite con il comando %token ad esse vengono assegnati id [identificativi] numerici a partire da 256.

Grazie a questo, è possibile avere tutti i caratteri ASCII come categorie. Diciamo che si voglia scrivere un calcolatore, sino ad ora avremmo scritto l'Analizzatore lessicale come segue:

[0-9]+          yylval=atoi(yytext); return NUMERO;
[ \n]+          /* ignora spazi bianchi */;
-               return MENO;
\*              return PER;
\+              return PIU;
...

La grammatica di YACC conterrebbe allora:

        exp:    NUMERO
                |
                exp PIU exp
                |
                exp MENO exp
                |
                exp PER exp

Questo è inutilmente complicato. Usando caratteri come abbreviazioni degli id dei simboli numerici, si può riscrivere l'Analizzatore lessicale come:

[0-9]+          yylval=atoi(yytext); return NUMERO;
[ \n]+          /* ignora spazi bianchi */;
.               return (int) yytext[0];

Quest'ultimo punto mette in corrispondenza tutti i singoli caratteri per i quali non è stata trovata una corrispondenza.

La grammatica per YACC sarebbe allora:

        exp:    NUMERO
                |
                exp '+' exp
                |
                exp '-' exp
                |
                exp '*' exp

Questo è molto più stringato e anche molto più chiaro. Non è necessario dichiarare questi simboli ascii con %token nella testata, sono definiti implicitamente.

Un'altra cosa molto utile a proposito di questo costrutto è che ora Lex non cercherà una corrispondenza a tutto quello che gli spediamo - evitando il comportamento di default per il quale tutto ciò che in input non trova una corrispondenza viene inviato in output identico. Se l'utente di questo calcolatore usa un ^, per esempio, ora darà un errore di analisi, invece di venir riflesso nello standard output.

6.2 Ricorsione: 'a destra è sbagliata' (right is wrong)

La ricorsione è un aspetto vitale di YACC. Senza di essa non si può specificare che un file consista di una sequenza di comandi indipendenti o asserzioni. Di per sé stesso, YACC è interessato solo alla prima regola, o a quella che si designa essere, con il simbolo '%start', la regola di partenza.

La ricorsione appare in YACC di due tipi: a destra e a sinistra. La ricorsione a sinistra, quella che si dovrebbe usare la maggior parte delle volte, assomiglia a quella che segue:

commands: /* vuoto */
        |
        commands command
Questa dice: un comando può o essere vuoto o consistere di più comandi seguiti da un comando. Per come lavora YACC, ciò significa che YACC può ora ritagliare gruppi individuali di comandi facilmente (dall'inizio) via via riducendoli.

Paragoniamola con la ricorsione a destra, che abbastanza stranamente a molte persone pare migliore:

commands: /* vuoto */
        |
        command commands
Ma questa è costosa. Se usata come regola %start, richiede a YACC di tenere tutti i comandi del vostro file nello stack, il che può richiedere parecchia memoria. Quindi si raccomanda caldamente di usare la ricorsione a sinistra quando si analizzino lunghe sequenze di comandi, come interi file. Alcune volte è difficile evitare la ricorsione a destra ma se le sequenze di comandi non sono troppo lunghe non è necessario fare i salti mortali per usare la ricorsione a sinistra.

Se si ha qualcosa che delimita (e quindi separa) i comandi, la ricorsione a destra appare molto naturale, ma è comunque costosa:

commands: /* vuoto */
        |
        command PUNTOEVIRGOLA commands

Il modo corretto per codificare in questo caso è usando la ricorsione a sinistra (non sono stato io ad inventarmi neppure questo):

commands: /* vuoto */
        |
        commands command PUNTOEVIRGOLA

Una versione precedente di questo HOWTO erroneamente usava la ricorsione a destra. Markus Triska gentilmente ce l'ha fatto notare.

6.3 Advanced yylval: %union

Attualmente, si deve definire *il* tipo di yylval. Questo però non va sempre bene. Ci sono situazioni in cui dobbiamo essere in grado di gestire tipi di dati multipli. Tornando al nostro ipotetico termostato, magari si vuole essere in grado di scegliere quale stufa si vuole controllare, così:

stufa edificioprincipale
        Selected 'edificioprincipale' stufa
obiettivo temperatura 23
        'edificioprincipale' stufa obiettivo temperatura adesso 23

Quello che si richiede qui è che yylval sia una 'union' che può contenere sia stringhe che numeri interi - ma non simultaneamente.

Si ricordi che in precedenza si era detto a YACC che tipo ci si aspettava che yylval fosse definendo YYSTYPE. Si può ragionevolmente definire YYSTYPE in modo che sia una 'union' nello stesso modo, ma YACC ha un metodo più semplice per fare questo: il comando %union.

Basandoci sull'esempio 4, si può ora scrivere la grammatica per YACC dell'Esempio 7. Prima l'introduzione:

%token TOKSTUFA TOKRISCALDAMENTO TOKOBIETTIVO TOKTEMPERATURA

%union
{
        int numero;
        char *stringa;
}

%token <numero> STATO
%token <numero> NUMERO
%token <stringa> PAROLA

Si è definita la 'union', che contiene soltanto un numero e una stringa. Poi usando una sintassi estesa di %token si è spiegato a YACC a quale parte della 'union' ciascuna categoria dovrebbe accedere.

In questo caso, si è permesso alla categoria STATO di usare un intero, come prima. La stessa cosa per il simbolo NUMERO che viene usato per leggere la temperatura.

Nuovo invece è il simbolo PAROLA, che è dichiarato necessitare una stringa.

Anche il file dell'Analizzatore lessicale (Lexer) cambia un po':

%{
#include <stdio.h>
#include <string.h>
#include "y.tab.h"
%}
%%
[0-9]+              yylval.numero=atoi(yytext); return NUMERO;
stufa               return TOKSTUFA;
riscaldamento       return TOKRISCALDAMENTO;
acceso|spento       yylval.numero=!strcmp(yytext,"acceso"); return STATO;
obiettivo           return TOKOBIETTIVO;
temperatura         return TOKTEMPERATURA;
[a-z0-9]+           yylval.stringa=strdup(yytext);return PAROLA;
\n                  /* ignora fine linea */;
[ \t]+              /* ignora spazi bianchi */;
%%

Come si può vedere, non si accede più direttamente a yylval, si aggiunge un suffisso che indica a quale parte di esso si vuole accedere. Non si ha bisogno di farlo nella grammatica per YACC però, visto che YACC compie la magia da sé:

scegli_stufa:
        TOKSTUFA PAROLA
        {
                printf("\tScelta stufa '%s'\n",$2);
                stufa=$2;
        }
        ;

A seguito della dichiarazione del %token di cui sopra, YACC sceglie automaticamente il membro 'stringa' dalla nostra 'union'. Si noti pure che si è memorizzata una copia di $2, che successivamente viene usata per dire all'utente a quale stufa sta mandando comandi:

target_set:
        TOKOBIETTIVO TOKTEMPERATURA NUMERO
        {
                printf("\tPer stufa '%s' temperatura impostata a %d\n",stufa,$3);
        }
        ;

Per maggiori dettagli leggere esempio7.y.


Avanti Indietro Indice