Introduction à Lex et Yacc -------------------------- Guillaume Thouvenin (22/02/2001) [Note: cet article provient de l'édition 2002-06 de LJNB, un journal qui n'existe plus.] 0.0 Introduction ================ Un compilateur est un programme qui lit un langage (la source) et qui le traduit par un programme équivalent dans un autre langage (la cible) en rapportant les erreurs éventuelles. Nous allons nous intéresser dans cette introduction à trois étapes qui sont l'analyse lexicale, l'analyse syntaxique et l'analyse sémantique. 0.1 Analyse lexicale -------------------- Le rôle de l'analyse lexicale est de fournir une suite de "tokens" (les éléments terminaux du langage) à partir d'une suite de caractères fournis en entrée. Par exemple, dans la déclaration suivante, x := 2*y + 3, l'analyseur lexical va identifier les caractères par : - Un identificateur x - Un symbole d'assignation := - Un nombre 2 - Le signe * - Un identificateur y - Le signe + - Un nombre 3 0.2 Analyse syntaxique ---------------------- L'analyse syntaxique est une analyse hiérarchique. L'objectif de cette analyse est de regrouper les "tokens" fournis par l'analyse lexicale en phrases grammaticales. En reprenant l'exemple précédent, nous pouvons construire l'arbre syntaxique abstrait suivant : assignation / | \ / | \ / := \ ident expression | / | \ | / | \ x expression + expression / | \ | / | \ | expression * expression nombre | | | | | | nombre ident 3 | | | | 2 y Dans cet exemple, les règles grammaticales associées à "expression" sont (":=" peut se traduire par "se dérive en"): expression := expression1 + expression2 expression := expression1 * expression2 expression := nombre expression := ident 0.3 Analyse sémantique --------------------- Enfin, l'analyse sémantique doit permettre de détecter les erreurs éventuelles. Il existe des outils permettant de générer automatiquement des analyseurs lexicaux et syntaxiques, nous allons vous présenter dans ce texte Lex (un analyseur lexical) et Yacc (un analyseur syntaxique). 1.0 Lex ======= 1.1 Théorie ----------- Comme nous l'avons indiqué dans l'introduction, Lex va lire des données afin de convertir les caractères en "tokens" compréhensibles par l'analyseur syntaxique. Pour permettre à Lex d'identifier des patrons de caractères, nous allons utiliser les expressions régulières. À chaque patron est associé une action (en général, il s'agit de retourner le "token" correspondant au patron). Par exemple, supposons qu'un identificateur soit de la forme "lettre(lettre|chiffre)*" c'est à dire une lettre suivit d'une lettre ou d'un chiffre zéro ou plusieurs fois (x, x1, xx, etc...). Il est alors possible de représenter ce patron par un automate à état fini (FSA : finite state automaton): lettre autre Début ----------> Etat0 ----------> Etat1 ----------> Fin | ^ | | | | \---/ lettre ou chiffre L'état "Fin" représente un état validant notre identificateur. De façon plus algorithmique, nous pourrions écrire : Début : aller à Etat0 Etat0 : lire c si c = lettre aller à Etat1 aller à Etat0 Etat1 : lire c si c = lettre aller à Etat1 si c = chiffre aller à Etat1 aller à Etat2 Fin : accepter l'identificateur C'est la technique utilisée par Lex. Les expressions régulières sont représentées sous forme d'un automate. 1.2 Reconnaissance des chaînes de caractères -------------------------------------------- Voici un tableau des "méta-caractères" utilisés par Lex : . n'importe quel caractère sauf le retour à la ligne \n retour à la ligne * zéro fois ou plus l'expression précédent ce signe + une fois ou plus l'expression précédent ce signe ? zéro ou une fois l'expression précédent ce signe ^ début de la ligne $ fin de la ligne a|b a ou b (ab)+ une fois ou plus le mot ab "a+b" la chaîne a+b [] une classe de caractères Quelques exemples : abc* ab, abc, abcc, abccc, ... [abc] a ou b ou c [a-z] une lettre entre a et z [a\-z] a, -, z [A-Za-z0-9]+ un caractère alphanumérique ou plus [^ab] n'importe quoi sauf a,b Nous pouvons remarquer que dans une classe de caractères, les "méta-caractères" perdent leur signification à l'exception de "^" et "-". Entre deux caractères, "-" signifie un intervalle et, lorsque "^" est utilisé en début d'une classe, il entraîne la négation de la classe. Si deux chaînes de caractères sont reconnues, Lex prendra la plus longue et si les deux chaînes ont la même longueur, Lex prendra celle se trouvant dans la règle la plus haute (la première déclarée). 1.3 La syntaxe de Lex --------------------- Lex peut être découpé en trois parties : ... les définitions ... %% ... les règles ... %% ... les sous-routines ... Le programme le plus simple que nous pouvons écrire est : %% Ce programme est tout à fait valable. Il va copier sur la sortie ce qui arrive en entrée un caractère après l'autre. Par défaut, l'entrée est "stdin" la sortie est "stdout". Lex possède des variables prédéfinies comme par exemple "yyleng" qui indique la longueur de la chaîne reconnue ou encore "yylval" qui est la valeur associée au "token". Une autre variable importante est "char *yytext" qui pointe sur le "patron" reconnu. Il existe aussi des fonctions prédéfinies comme "int yylex(void)" qui permet d'invoquer l'analyseur lexicale et qui retourne un "token". Regardons maintenant un exemple plus intéressant qui permet de numéroter un texte: exemple1.l - - - - - %{ int yylineno; %} %% ^(.*)\n printf("%4d\t%s", ++yylineno, yytext); %% int main(int argc, char *argv[]) { yyin = fopen(argv[1], "r"); yylex(); fclose(yyin); } Le code de la partie définition va être copié tel quel dans le fichier C généré par Lex et ce code doit être délimité par %{ et %}. La partie définition peut aussi contenir des substitutions permettant d'améliorer la lisibilité de notre programme. Par exemple, il est possible d'avoir une partie définition tel que : exemple2.l - - - - - chiffre [0-9] lettre [A-Za-z] %{ int count; %} %% {lettre}({lettre}|{chiffre})* count++; %% int main(void) { yylex(); printf("nombre d'identificateurs = %d\n", count); return 0; } Remarquons dans l'exemple1.l que l'entrée utilisée par Lex n'est plus celle par défaut (stdin) mais, argv[1]. Il est bien sûr possible de faire la même chose pour la sortie en utilisant la variable yyout. Dans la pratique, voici la façon permettant d'obtenir un exécutable à partir de l'exemple1.l : guill:~/parserTest/simple $ flex exemple1.l guill:~/parserTest/simple $ ls -l total 164 drwxr-xr-x 3 guill users 4096 Feb 22 15:09 . drwxr-xr-x 4 guill users 4096 Feb 19 20:06 .. -rwxr-xr-x 1 guill users 24226 Feb 21 16:28 calc -rw-r--r-- 1 guill users 211 Feb 21 16:01 calc.l -rw-r--r-- 1 guill users 330 Feb 21 16:28 calc.y -rw-r--r-- 1 guill users 181 Feb 22 15:07 exemple1.l drwxr-xr-x 2 guill users 4096 Feb 18 14:16 julesCesar -rw-r--r-- 1 guill users 36044 Feb 22 15:09 lex.yy.c -rwxr-xr-x 1 guill users 19974 Feb 18 14:04 simple -rw-r--r-- 1 guill users 162 Feb 18 14:04 simple.l -rwxr-xr-x 1 guill users 20479 Feb 15 18:36 simple2 -rw-r--r-- 1 guill users 179 Feb 15 18:36 simple2.l -rw-r--r-- 1 guill users 2131 Feb 21 16:28 y.output -rw-r--r-- 1 guill users 23243 Feb 21 16:28 y.tab.c -rw-r--r-- 1 guill users 88 Feb 21 16:28 y.tab.h guill:~/parserTest/simple $ gcc -o exemple1 lex.yy.c -lfl guill:~/parserTest/simple $ exemple1 < exemple1.l 1 %{ 2 int yylineno; 3 %} 4 5 %% 6 7 ^(.*)\n printf("%4d\t%s", ++yylineno, yytext); 8 9 %% 10 11 int main(int argc, char *argv[]) { 12 yyin = fopen(argv[1], "r"); 13 yylex(); 14 fclose(yyin); 15 } guill:~/parserTest/simple $ Nous avons utilisé Flex à la place de Lex. Si vous utilisez Lex, il faut alors utiliser la librairie Lex (-ll) au lieu de la librarie Flex (-lfl). 2.0 YACC ======== 2.1 Théorie ----------- Un langage de programmation peut être défini par sa syntaxe et, par sa sémantique. Pour spécifier la syntaxe d'un langage, il est possible d'utiliser ce que l'on appelle les grammaires libres de contexte (context-free) ou BNF (forme de Backus-Norm). Une grammaire libre de contexte possède quatre composantes qui sont : - Un ensemble de "tokens" ou terminaux du langage - Un ensemble de non-terminaux - Un ensemble de règles de productions ou chacune des règles consistent en un non-terminal à gauche de la production (left-side), une flèche et une séquence de tokens et/ou de non-terminaux à droite de la production (right-side) - Un non-terminal de départ. Les grammaires pour Yacc sont décrites en utilisant cette forme. La plupart des langages modernes peuvent être décris sous cette forme. Prenons par exemple la grammaire suivante : (r1) E -> E + E (r2) E -> E * E (r3) E -> id Nous venons de spécifier trois règles de production. Cette grammaire indique qu'une expression peut être la somme de deux expressions, le produit de deux expressions ou un identificateur. Nous pouvons utiliser cette grammaire pour générer l'expression suivante : E -> E * E (r2) -> E * z (r3) -> E + E * z (r1) -> E + y * z (r3) -> x + y * z (r3) A chaque étape, nous avons remplacé le membre droit d'une production avec le membre gauche de cette même production (les productions sont notées r1, r2 et r3). Pour "parser" une expression, Yacc a besoin de faire l'opération inverse. Plutôt que de partir d'un non-terminal et générer une expression à partir de la grammaire, il faut partir d'une expression et la réduire à un non-terminal. Cette technique s'appelle un "parsage bottom-up" ou "shift-reduce" et, elle utilise des piles pour stocker les termes. Voici la même dérivation que précédemment mais en bottom-up : 1 .x + y * z (shift) 2 x .+ y * z (reduce [r3]) 3 E .+ y * z (shift) 4 E + .y * z (shift) 5 E + y .* z (reduce [r3]) 6 E + E .* z (shift) 7 E + E * .z (shift) 8 E + E * z. (reduce [r3]) 9 E + E * E. (reduce [r2]) 10 E + E. (reduce [r1]) 11 E. (accept) Le "." symbolise le "lookhead" qui est le symbole courant. Le terme à gauche du "." est sur la pile. Lorsque le sommet de la pile correspond à un terme se trouvant à droite d'une production, on remplace le token correspondant avec la partie gauche de la règle de production. A l'étape 1, on place x sur la pile. L'étape 2 applique la règle r3 et, x est donc remplacé par la partie gauche de la règle 3 (c'est à dire E). Nous continuons le processus jusqu'à ce qu'il n'y ait plus qu'un seul non-terminal sur la pile. Dans la grammaire que nous avons utilisé, nous pouvons remarquer des conflits. Par exemple, à l'étape 6, nous avons choisi de décaler le lookhead après l'opérateur '*' alors que nous aurions pu faire une réduction de "E + E" en utilisant la règle r1. Nous avons donc un conflit "shift-reduce". Ceci arrive car notre grammaire est ambiguë (il existe plusieurs dérivations possibles). Nous aurions pu avoir aussi un conflit "reduce-reduce" comme par exemple dans la grammaire suivante : E -> T E -> id T -> id Yacc résout ces problèmes en adoptant une action par défaut. Dans le cas d'un conflit "shift-reduce", Yacc va choisir le "shift". Dans le cas d'un conflit "reduce-reduce", Yacc va choisir la première règle dans la grammaire. En cas de conflits, Yacc génère des "warnings" sur les règles problématiques. Il est aussi possible de lui spécifier un comportement. 2.2 La syntaxe de Yacc ---------------------- Les entrées dans Yacc sont diviseés en trois parties. ... définitions ... %% ... règles ... %% ... sous-routines ... La section définitions contient la déclaration des "tokens" et peut aussi contenir du code C qui doit alors être placé entre "%{" et "%}". Les règles de grammaire se retrouvent dans la partie "règles" et enfin, les fonctions utilisées par l'usager vont se placer dans la partie réservée aux sous-routines. Pour commencer, nous allons prendre comme exemple un simple calculateur qui permet d'ajouter et de soustraire des nombres afin de mettre en évidence le lien entre Lex et Yacc. Dans la partie définition de Yacc, nous pouvons placer : % token INTEGER Cette définition déclare INTEGER comme étant un "token" de notre langage. Lorsque l'on exécute Yacc, il génère un parser dans le fichier y.tab.c et, en utilisant l'option -d, il crée un fichier d'en-tête y.tab.h qui ressemble à : #ifndef YYSTYPE #define YYSTYPE int #endif #define INTEGER 258 extern YYSTYPE yylval; Le fichier y.tab.h va être inclus dans la partie définition de notre fichier lex et Lex va pouvoir utiliser la définition de INTEGER. Pour obtenir le "token", Yacc fait un appel à la fonction yylex(). Cette fonction retourne un entier qui est le token. Les valeurs associées aux "tokens" sont retournées par l'intermédiaire de la variable yylval. Par exemple, dans notre fichier lex nous pourrions avoir : [0-9]+ { yylval = atoi(yytext); return INTEGER; } La valeur associée au "token" INTEGER est placée dans yyval et Lex retourne le "token" INTEGER. Les valeurs de "tokens" 0-255 sont réservées pour les caractères. En générale, Yacc commence la numérotation des tokens à 258. Voyons maintenant un exemple plus complet de notre calculateur : %{ #include "y.tab.h" %} %% [0-9]+ { yylval = atoi(yytext); return INTEGER; } [-+\n] return *yytext; [ \t] ; /* On ignore les espaces */ . yyerror("Caractère inconnu."); %% int yywrap(void) { return 1; } Dans le cas de "+" et "-", c'est la valeur du caractère qui est retournée. La fonction yywrap() est appelée lorsque tous les caractères en entrée sont lus. Intéressons nous maintenant à la partie Yacc. De façon interne, Yacc utilise deux piles en mémoire, la pile de "parsage" et la pile de valeur. La pile de "parsage" contient les terminaux et non-terminaux et représente l'état courant d'analyse. La pile de valeur est un tableau d'éléments de type YYSTYPE et, elle permet d'associer une valeur à chaque élément de la pile de "parsage". Par exemple, lorsque Lex retourne le token "INTEGER", Yacc empile ce "token" sur la pile de "parsage". De façon synchrone, la valeur associée à ce "token" est placée sur la pile de valeur. Il faut bien comprendre que les deux piles sont synchronisées et donc, trouver la valeur associée à un token est très facile. Par exemple, supposons que Lex soit en train de lire la valeur 69, les piles auront donc à leurs sommets 69 pour la pile de valeur et 258 (INTEGER) pour la pile de "parsage". Continuons notre exemple du calculateur avec le fichier Yacc correspondant : %token INTEGER %% programme : programme expr '\n' { printf("Result : %d\n", $2);} | ; expr : INTEGER { $$ = $1;} | expr '+' expr { $$ = $1 + $3;} | expr '-' expr { $$ = $1 - $3;} ; %% int yyerror (char *s) { fprintf(stderr, "%s\n", s); return 0; } int main(void) { yyparse(); return 0; } Pour compiler cet exemple, nous avons utilisé Flex et Bison (les successeurs de Lex et Yacc). Voyons le résultat : guill:~/parserTest/simple $ bison -d -o y.tab.c calc.y calc.y contains 4 shift/reduce conflicts. guill:~/parserTest/simple $ flex calc.l guill:~/parserTest/simple $ gcc -o calc lex.yy.c y.tab.c -lfl guill:~/parserTest/simple $ ./calc 1+2 Result : 3 12*4 Caractère inconnu. parse error Tout d'abord, l'option -d permet la génération du fichier y.yab.h. Ensuite, contrairement à Yacc, Bison produit par défaut un fichier C nommé calc.tab.c et un fichier H nommé calc.tab.h (Yacc produit un fichier y.tab.c et un fichier y.tab.h). Or, comme nous avions déclaré un fichier y.tab.h dans notre fichier lex, nous avons choisi de définir le fichier de sortie comme étant y.tab.c afin d'obtenir un fichier y.tab.h (À part ça, tout ce que nous avons dit pour Lex et Yacc est valable pour Flex et Bison). Bison nous indique que notre grammaire contient quatre conflits "shift/reduce" qui sont réglés comme nous l'avons expliqué dans la partie 2.1. Pour produire notre exécutable "calc", nous indiquons à gcc d'utiliser la librairie de Flex (-lfl). Dans notre exemple, nous avons essayé d'évaluer l'expression 12*4. '*' est retourné comme étant inconnu par Lex ce qui conduit à une erreur de parsage. Il est aussi possible de voir la table de parsage générée par Yacc en ajoutant l'option -v lors de la compilation (qui entraîne la création d'un fichier y.output). Voici un exemple de ce que contient ce fichier : State 7 contains 2 shift/reduce conflicts. State 8 contains 2 shift/reduce conflicts. Grammar rule 1 programme -> programme expr '\n' rule 2 programme -> /* empty */ rule 3 expr -> INTEGER rule 4 expr -> expr '+' expr rule 5 expr -> expr '-' expr Terminals, with rules where they appear $ (-1) '\n' (10) 1 '+' (43) 4 '-' (45) 5 error (256) INTEGER (257) 3 Nonterminals, with rules where they appear programme (7) on left: 1 2, on right: 1 expr (8) on left: 3 4 5, on right: 1 4 5 ... Revenons sur notre fichier Yacc. La section "règles" correspond à une grammaire BNF. La partie droite d'une production apparaît justifiée à gauche et est suivie de ":". Ensuite, nous plaçons la partie droite de notre production. Les actions apparaissent entre des accolades. Lorsque nous appliquons la règle : expr : expr '+' expr { $$ = $1 + $3; } nous remplaçons la partie droite de la production par la partie gauche de cette même production. Dans notre cas, nous enlevons expr '+' expr de la pile puis nous plaçons expr sur la pile. Nous avons donc réduit la pile en enlevant trois éléments et en en replaçant un. Nous indiquons la position, dans la pile de valeur, du premier élément de la partie droite de la production par $1, le second par $2, etc... $$ représente le sommet de la pile après la réduction. Pour savoir comment faire les réductions ou les décalages, Yacc construit une table de parsage dite LALR (Lookahead-LR). Voici la table de parsage générée par Yacc pour notre calculateur : -------------------------------------------------------------------------- | | ACTIONS | GOTO | | STATE |-----------------------------------------------|------------------| | | \n | + | - | error | INTEGER | $ | $Défaut | programme | expr | |-------------------------------------------------------------------------- | 0 | | | | | | | r2 | 1 | | | 1 | | | | | s2 | 9 | | | 3 | | 2 | | | | | | | r3 | | | | 3 | s4 | s5 | s6 | | | | | | | | 4 | | | | | | | r1 | | | | 5 | | | | | s2 | | | | 7 | | 6 | | | | | s2 | | | | 8 | | 7 | | s5 | s6 | | | | r4 | | | | 8 | | s5 | s6 | | | | | | | | 9 | | | | | | 10 | | | | | 10 | | | | | | | accept | | | -------------------------------------------------------------------------- 1. "si" indique un décalage (shift) et un passage de la pile à l'état i 2. "rj" indique une réduction (reduce) par la production numéro j 3. "accept" indique un succès 4. un "blanc" indique une erreur de parsage Yacc ajoute aux terminaux une action par défaut qui est exécutée si aucuns "tokens" ne correspond à la valeur attendue. Par exemple, si nous sommes dans l'état 7, si nous rencontrons un "token" différent de '+' ou '-', nous effectuerons alors une réduction en utilisant la règle 4 (cf. l'exemple du fichier y.output). Prenons par exemple la chaîne "12+3+4\n$" (ce qui est équivalent à saisir 12+3+4 'entrée' dans notre programme calc). Lorsque Yacc commence l'analyse, nous sommes dans l'état 0, et avant même de saisir un texte, il exécute l'action par défaut qui est une réduction en utilisant la règle 1 (programme -> /*empty*/). Nous sommes donc dans l'état 0 avec le non-terminal "programme". Comme nous venons de faire une réduction, Yacc va aller dans un nouvel état en regardant la partie GOTO de la table de parsage. La table indique d'aller à l'état 1. Ceci peut-être considéré comme une initialisation. À ce moment, Yacc est prêt à commencer l'analyse. Il va faire appel à Lex pour récupérer un "token". Lex va donc retourner le "token" INTEGER en mettant la variable yylval à 12. Yacc récupère donc le "token" 257 (qui correspond à INTEGER). Il regarde dans sa table de parsage (l'état courant est l'état 1). Nous voyons que l'action à effectuer est un déplacement vers la droite (shift) et un passage dans l'état 2. Nous avons donc nos piles dans les états suivants : État : 0 - 1 - 2 Parse Stack : INTEGER | Value Stack : 12 | Nous venons de faire un "shift", nous allons donc déplacer le "lookhead" (qui va devenir '+') et l'état va passer à 2. Dans l'état 2, nous voyons que peut importe le "token", la seule action possible est une réduction en utilisant la règle 3 (expr -> INTEGER). Les piles vont donc être modifiées de la façon suivante : État : 0 - 1 Parse Stack : expr | Value Stack : 12 | La valeur 12 reste dans la pile car nous avons dans notre grammaire indiqué la règle {$$ = $1;} ce qui a pour effet d'enlever 12 de la pile pour le remettre. Nous venons d'effectuer une réduction, il faut donc regarder dans la table l'état dans lequel nous allons aller. L'état courant est 1 (2 a été enlevé lors de la réduction), la pile de "parsage" contient expr au sommet, la partie goto indique que nous allons aller à l'état 3. Au niveau de l'état 3, nous voyons que le symbole ('+') implique un décalage et un passage à l'état 5. Le token courant est donc INTEGER (3) et les piles sont donc maintenant : État : 0 - 1 - 3 - 5 Parse Stack : expr | token + | Value Stack : 12 | '+' | Remarquons que dans l'état 5, un autre token que INTEGER provoque une erreur de parsage. La lecture du token INTEGER à l'état 5 entraîne un déplacement du lookhead et un passage à l'état 2. Comme précédemment, dans l'état, nous avons une action par défaut nous imposant une réduction avec la règle 3. Après cette réduction, nous allons avoir le non-terminal "expr" en haut de la pile et, l'état courant positionné à 5 et donc, la partie GOTO nous indique d'aller dans l'état 7 : État : 0 - 1 - 3 - 5 - 7 Parse Stack : expr | token + | expr Value Stack : 12 | '+' | 3 En continuant ainsi, nous allons pouvoir analyser toute la chaîne "12+3+4". Vous pouvez voir ce processus en utilisant l'option de deboggage de Yacc. Starting parse Entering state 0 Reducing via rule 2 (line 11), -> programme state stack now 0 Entering state 1 Reading a token: 12+3+4 Next token is 257 (INTEGER) Shifting token 257 (INTEGER), Entering state 2 Reducing via rule 3 (line 14), INTEGER -> expr state stack now 0 1 Entering state 3 Reading a token: Next token is 43 ('+') Shifting token 43 ('+'), Entering state 5 Reading a token: Next token is 257 (INTEGER) Shifting token 257 (INTEGER), Entering state 2 Reducing via rule 3 (line 14), INTEGER -> expr state stack now 0 1 3 5 Entering state 7 Reading a token: Next token is 43 ('+') Shifting token 43 ('+'), Entering state 5 Reading a token: Next token is 257 (INTEGER) Shifting token 257 (INTEGER), Entering state 2 Reducing via rule 3 (line 14), INTEGER -> expr state stack now 0 1 3 5 7 5 Entering state 7 Reading a token: Next token is 10 ('\n') Reducing via rule 4 (line 16), expr '+' expr -> expr state stack now 0 1 3 5 Entering state 7 Next token is 10 ('\n') Reducing via rule 4 (line 16), expr '+' expr -> expr state stack now 0 1 Entering state 3 Next token is 10 ('\n') Shifting token 10 ('\n'), Entering state 4 Reducing via rule 1 (line 9), programme expr '\n' -> programme Result : 19 state stack now 0 Entering state 1 2.3 Un exemple plus complet --------------------------- Pour terminer, nous allons rapidement commenté un exemple plus complet dont les sources sont disponibles à la fin de ce texte. Il s'agit d'un calculateur acceptant certaines structures du type boucle et, il accepte aussi des variables (les variables n'ayant qu'un seul caractère). Nous avons utilisé Flex et Bison pour produire les fichiers C. Si vous utilisez Lex et Yacc, il faudra modifier le nom du fichier d'en-tête en "y.tab.h". Le principe est simple. Nous allons construire une liste au fur et à mesure du parcours de notre grammaire. Cette liste sera évaluée lorsque nous aurons réussi à réduire notre programme jusqu'à la racine. Nous voyons donc que nous allons travailler avec trois types possible dans la pile de valeur. En effet, nous pouvons avoir un entier, un caractère ou un noeud. Pour prévenir Yacc qu'il va y avoir trois types possibles, nous allons déclarer ces différents type dans une union dans la partie définition de Yacc : %union { int iValue; /* Notre type entier */ char sIndex; /* Notre type caractère */ nodeType *nPtr; /* Notre type noeud */ } Ceci va avoir pour effet de déclarer YYSTYPE de la façon suivante : typedef union { int iValue; char sIndex; nodeType *nPtr; } YYSTYPE; Et, Yacc va ensuite déclarer une variable yyvalue comme étant de type YYSTYPE. Nous pouvons donc maintenant empiler un entier, un caractère ou un noeud dans la pile de valeur. Nous remarquons aussi dans notre code les déclarations : %token <iValue> INTEGER %token <sIndex> VARIABLE Ceci nous permet de lier le token INTEGER au type entier et, le token VARIABLE au type caractère. Ceci permet à Yacc de savoir le type à utiliser dans une production comme : expr : INTEGER {$$ = con($1);} qui va produire le code C : yylval.nPtr = con(yyvsp[0].iValue); L'opérateur '-' unaire a une priorité supérieure aux opérateurs binaires grâce à l'utilisation de %nonassoc UMINUS. Cette règle indique que l'opérateur UMINUS n'est pas associatif. De plus, cette règle utilisée avec : %prec (expr : '-' expr %prec UMINUS ...) implique que UMINUS a une priorité supérieur aux autres. C'est la même technique qui est utilisée pour lever l'ambiguïté sur le IF-ELSE. L'ambiguïté sur le IF-ELSE arrive dans le cas suivant : Soit la règle stmt : IF expr stmt | IF expr stmt ELSE stmt ... et nous devons analyser la déclaration suivante : IF expr stmt IF expr stmt . ELSE stmt Le "." est le symbole qui est en train d'être lu. Dans ce cas, doit-on réduire ou bien avancer. Si nous réduisons nous obtenons IF expr stmt ELSE stmt. Si nous avançons, nous obtenons IF expr stmt stmt. Dans les langages modernes, le ELSE est lié au plus récent IF. Il faut donc avancer et non pas réduire. Par défaut Yacc a le bon comportement mais, pour enlever le "warning", nous avons donné au IF-ELSE une plus haute "précedence" que le simple IF. Enfin, l'arbre syntaxique est construit "bottom-up", allouant des feuilles lorsqu'un entier ou un caractère est réduit. Lorsqu'un opérateur est rencontré, un noeud est alloué et des pointeurs sur les noeuds précédemment créés sont placés comme opérateurs. Lorsque nous avons remonté l'arbre, la fonction ex() est appelée et effectue un parcours en profondeur "depth-first" de l'arbre. 3.0 Conclusion ============== Si vous souhaitez voir ce que l'on peut faire sur un vrai langage comme le C, nous vous conseillons d'aller voir le code source de LCLint qui utilise Flex et Bison et analyse du code C. Le code de LCLint est bien écrit et est donc compréhensible. Fichier test ============ x = 0; while ( x < 3 ) { printf x; x = x + 1; } Fichier calc3.h =============== typedef enum { typeCon, typeId, typeOpr } nodeEnum; /* Constants */ typedef struct { nodeEnum type; /* type of node */ int value; /* value of constant */ } conNodeType; /* Identifiers */ typedef struct { nodeEnum type; /* type of node */ int i; /* subscript to ident array */ } idNodeType; /* Operators */ typedef struct { nodeEnum type; /* type of node */ int oper; /* operator */ int nops; /* number of operands */ union nodeTypeTag *op[1]; /* operands (expandable) */ } oprNodeType; typedef union nodeTypeTag { nodeEnum type; /* type of node */ conNodeType con; /* constants */ idNodeType id; /* identifiers */ oprNodeType opr; /* operators */ } nodeType; extern int sym[26]; Fichier calculator.l ==================== %{ #include <stdio.h> #include "calc3.h" #include "calculator.tab.h" %} %% [a-z] { yylval.sIndex = *yytext - 'a'; return VARIABLE; } [0-9]+ { yylval.iValue = atoi(yytext); return INTEGER; } [-()<>=+*/;{}] { return *yytext; } ">=" { return GE; } "<=" { return LE;} "==" { return EQ;} "!=" { return NE;} "while" { return WHILE;} "if" { return IF;} "else" { return ELSE;} "printf" { return PRINTF;} [ \t\n]+ ; . yyerror("Invalid character\n"); %% int yywrap(void) { return 1; } Fichier calculator.y ==================== %{ #include <stdio.h> #include <stdlib.h> #include <stdarg.h> #include "calc3.h" /* prototypes */ nodeType* opr(int oper, int nops, ...); nodeType* id(int i); nodeType* con(int value); int ex(nodeType *p); void freeNode(nodeType *p); void yyerror(char *s); int sym[26]; /* table des symboles */ %} %union { int iValue; char sIndex; nodeType *nPtr; }; %token <iValue> INTEGER %token <sIndex> VARIABLE %token WHILE IF PRINTF %nonassoc IFX %nonassoc ELSE %left GE LE EQ NE '>' '<' %left '-' '+' %left '*' '/' %nonassoc UMINUS %type <nPtr> stmt expr stmt_list %% program : function { fprintf(stderr, "regle 1 \n"); exit(0); } ; function : function stmt { fprintf(stderr, "regle 2 \n"); fprintf(stdout, " RES = %d\n", ex($2)); freeNode($2);} | /* NULL */ ; stmt : ';' { fprintf(stderr, "regle 3 \n"); $$ = opr(';', 2, NULL, NULL);} | expr ';' { fprintf(stderr, "regle 4 \n"); $$ = $1;} | PRINTF expr ';' { fprintf(stderr, "regle 5 \n"); $$ = opr(PRINTF, 1, $2);} | VARIABLE '=' expr ';' { fprintf(stderr, "regle 6 \n"); $$ = opr('=', 2, id($1), $3);} | WHILE '(' expr ')' stmt { fprintf(stderr, "regle 7 \n"); $$ = opr(WHILE, 2, $3, $5);} | IF '(' expr ')' stmt %prec IFX { fprintf(stderr, "regle 8 \n"); $$ = opr(IF, 2, $3, $5);} | IF '(' expr ')' stmt ELSE stmt { fprintf(stderr, "regle 9 \n"); $$ = opr(IF, 2, $3, $5, $7);} | '{' stmt_list '}' { fprintf(stderr, "regle 10 \n"); $$ = $2;} ; stmt_list : stmt { fprintf(stderr, "regle 11 \n"); $$ = $1;} | stmt_list stmt { fprintf(stderr, "regle 12 \n"); $$ = opr(';', 2, $1, $2);} ; expr : INTEGER { fprintf(stderr, "regle 13 \n"); $$ = con($1);} | VARIABLE { fprintf(stderr, "regle 14 \n"); $$ = id($1);} | '-' expr %prec UMINUS { fprintf(stderr, "regle 15 \n"); $$ = opr(UMINUS, 1, $2);} | expr '+' expr { fprintf(stderr, "regle 16 \n"); $$ = opr('+', 2, $1, $3);} | expr '-' expr { fprintf(stderr, "regle 17 \n"); $$ = opr('-', 2, $1, $3);} | expr '*' expr { fprintf(stderr, "regle 18 \n"); $$ = opr('*', 2, $1, $3);} | expr '/' expr { fprintf(stderr, "regle 19 \n"); $$ = opr('/', 2, $1, $3);} | expr '<' expr { fprintf(stderr, "regle 20 \n"); $$ = opr('<', 2, $1, $3);} | expr '>' expr { fprintf(stderr, "regle 21 \n"); $$ = opr('>', 2, $1, $3);} | expr GE expr { fprintf(stderr, "regle 22 \n"); $$ = opr(GE, 2, $1, $3);} | expr LE expr { fprintf(stderr, "regle 23 \n"); $$ = opr(LE, 2, $1, $3);} | expr NE expr { fprintf(stderr, "regle 24 \n"); $$ = opr(NE, 2, $1, $3);} | expr EQ expr { fprintf(stderr, "regle 25 \n"); $$ = opr(EQ, 2, $1, $3);} | '(' expr ')' { fprintf(stderr, "regle 26 \n"); $$ = $2;} ; %% nodeType *con(int value) { nodeType *p; /* allocate node */ if ((p = malloc(sizeof(conNodeType))) == NULL) yyerror("out of memory"); /* copy information */ p->type = typeCon; p->con.value = value; return p; } nodeType *id(int i) { nodeType *p; /* allocate node */ if ((p = malloc(sizeof(idNodeType))) == NULL) yyerror("out of memory"); /* copy information */ p->type = typeId; p->id.i = i; return p; } nodeType *opr(int oper, int nops, ...) { va_list ap; nodeType *p; size_t size; int i; /* allocate node */ size = sizeof(oprNodeType) + (nops - 1) * sizeof(nodeType *); if ((p = malloc(size)) == NULL) yyerror("out of memory"); /* copy information */ p->type = typeOpr; p->opr.oper = oper; p->opr.nops = nops; va_start(ap, nops); for (i = 0; i < nops; i++) p->opr.op[i] = va_arg(ap, nodeType*); va_end(ap); return p; } void freeNode(nodeType *p) { int i; if (!p) return; if (p->type == typeOpr) { for (i=0; i < p->opr.nops; i++) freeNode(p->opr.op[i]); } free(p); } int ex(nodeType *p) { if (!p) return 0; switch(p->type) { case typeCon : return p->con.value; case typeId : return sym[p->id.i]; case typeOpr : switch(p->opr.oper) { case WHILE : while (ex(p->opr.op[0])) ex(p->opr.op[1]); return 0; case IF : if (ex(p->opr.op[0])) ex(p->opr.op[1]); else if (p->opr.nops > 2) ex(p->opr.op[2]); return 0; case PRINTF : printf("%d\n", ex(p->opr.op[0])); return 0; case ';' : ex(p->opr.op[0]); return ex(p->opr.op[1]); case '=' : return sym[p->opr.op[0]->id.i] = ex(p->opr.op[1]); case UMINUS : return -ex(p->opr.op[0]); case '+' : return ex(p->opr.op[0]) + ex(p->opr.op[1]); case '-' : return ex(p->opr.op[0]) - ex(p->opr.op[1]); case '*' : return ex(p->opr.op[0]) * ex(p->opr.op[1]); case '/' : return ex(p->opr.op[0]) / ex(p->opr.op[1]); case '<' : return ex(p->opr.op[0]) < ex(p->opr.op[1]); case '>' : return ex(p->opr.op[0]) > ex(p->opr.op[1]); case GE : return ex(p->opr.op[0]) >= ex(p->opr.op[1]); case LE : return ex(p->opr.op[0]) <= ex(p->opr.op[1]); case NE : return ex(p->opr.op[0]) != ex(p->opr.op[1]); case EQ : return ex(p->opr.op[0]) == ex(p->opr.op[1]); } } } void yyerror(char *s) { fprintf(stderr, "%s\n", s); } int main (void) { yyparse(); return 0; }