"ExoBison" : Exercices Bison + Flex(Corrigé)



0. Sommaire
1. Listes
2. GaBuZoMeu
3. Calculatrice
4. Arbres de Syntaxe
5. Yet Anothor CalC
6. Passage de Chaînes
7. Titre du WSJ
8. Langage naturel

0. Sommaire et Objectifs

  1. Grammaires de listes

    Maîtriser l'écriture d'une grammaire pour une liste, récursivité droite et gauche, grammaire ambiguë.

  2. GaBuZoMeu, ou langage de Dyck

    Vérifier la maîtrise des grammaires de listes, exemple important en théorie des langages.

  3. Calculatrice Arithmétique

    Exemple complet d'analyse syntaxique, couplage Flex/Bison avec passage de valeur, utilisation de priorités, table de symboles

  4. Arbres de Syntaxe Abstraite: Calculatrice vs. Compilateur

    Construction explicite des Arbres de Syntaxe Abstraite (AST)

  5. Encore des calculettes (Yet Another CalC !)

    Des variations de "Calculatrice Arithmétique"

  6. Passage de Chaînes de caractères entre Flex et Bison - Corrigé

    Passage de valeur "char *" : yytext vs strdup

  7. Titre du WallStreetJournal

    Grammaire pour langage naturel, continuation exemple du cours

  8. Langage naturel élémentaire, construction de structures

    Grammaire pour langage naturel, passage de valeur "char *", gestion de structures à base de "char *".


1. Grammaires de listes

On se donne un analyseur lexical simple list.flex qui lit des entiers (Token TOK), ignore les blancs, et remonte les autres caractères (y compris '\n').
Cette analyseur passe aussi dans la variable int yylval, la valeur associées aux tokens entier TOK. Il n'y a pas besoin de déclaration %union dans Bison pour accéder à cette valeur avec un $i dans les actions Bison.

On désire tester différentes grammaires de listes.

1.1) Squelette d'analyse ligne à ligne

  1. Écrire un premier analyseur syntaxique qui analyse "ligne à ligne", c'est à dire répond pour chaque ligne "ligne correcte" ou "ligne erronée".
    Pour tester, on prendra comme lignes correctes des lignes contenant uniquement un token entier TOK.

    Vérifier le bon fonctionnement de la gestion d'erreur et en particulier que l'analyseur sait analyser une ligne correcte après avoir traité une ligne erronée

    Corrigé :

    token TOK '\n'
    %%
    Line : /*vide*/
         | Line list '\n'  {printf("Ligne OK\n");}
         | Line error '\n' {yyerrok; printf("Ligne Fausse\n");}
    ;
    list : TOK
    %%
    
  2. Pour tester différentes grammaires en même temps dans une seule spécification Bison, on propose qu'une ligne correcte commence par un caractère ('a', 'b',...) suivi d'un symbole non-terminal à tester (lista,listb,...)

    Écrire par exemple, l'analyseur qui lit des lignes 'a' contenant 1 entier et des lignes b contenant 2 entiers.

    a 11         =OK
    b 11 12      =OK
    a 11 12      =FAUX
    a 1          =OK
                 =FAUX
    

    Corrigé :

    token TOK '\n'
    %%
    Line : /* vide */
         | Line list '\n'  {printf("Ligne OK\n");}
         | Line 'a' lista '\n'  {printf("lista OK\n");}
         | Line 'b' listb '\n'  {printf("listb OK\n");}
         | Line error '\n' {yyerrok; printf("Ligne Fausse\n");}
    ;
    list : TOK
    lista : TOK
    listb : TOK TOK
    %%
    

1.2) Listes et récursivité droite, gauche,...

On désire reconnaître une liste d'entier. Tester et comparer les différentes grammaires :

%%
lista  : TOK
       | lista TOK
;
Listb  : TOK
       | TOK Listb
;
Listc : TOK
       | Listc Listc
;
Listd  : 
       | Listd TOK
;
%%

NOTA BENE :

Corrigé

%%  /* tracer ordre d'analyse des tokens */
...
lista  : TOK       {printf(" =%d "),$1);}
       | lista TOK {printf(" %d  ",$2);}
;
...
%%

1.3) Liste avec séparateur

Écrire et Tester des grammaires pour une liste d'entiers séparés par des virgules. Cela peut par exemple servir pour l'analyse des arguments d'une fonction, ou aussi pour l'analyse d'une constante de type ensemble ({}, {2,5,6} ...)... On envisagera les cas de listes non-vides ou de listes éventuellement vides, On pourra aussi décliner l'écriture de la grammaire sous les formes récursives gauches ou droites.

Pourquoi la grammaire suivante est incorrecte (par rapport à l'usage courant) ? Écrire et tester une version correcte.

%%
listg : /* Vide */
      | listg ',' TOK
;
%%

Corrigé :

%%  
liste  : TOK            /* liste non vide RecGauche */
       | liste ',' TOK
listf  : TOK            /* liste non vide RecDroit */
       | TOK ',' listf
listg  :  /* mot vide */   /* liste eventuellement vide gauche FAUSSE */
       | listg ',' TOK
listh  :  /* mot vide */   /* liste eventuellement vide droite FAUSSE */
       | TOK ',' listh
listi  : /* mot vide */    /* liste eventuellement vide gauche CORRECT */
       | liste
%%

1.4)Liste avec terminateur

Écrire une grammaire récursive droite et une grammaire récursive gauche pour reconnaître une liste éventuellement vide d'entiers chacun suivi d'un ';'.
Ceci correspond par exemple à la définition d'un programme comme une séquence d'instructions terminées par un ';'.
C'est aussi le schéma de l'analyse "ligne à ligne" utilisée depuis le début de l'exercice.

Corrigé:

%%  
listk  : /*vide */       /* liste eventuellement vide rec. gauche */
       | listk TOK ';' {printf(GREEN("%d "),$2);}
;
listj  : /*vide */       /* liste eventuellement vide rec. droit */
       | TOK ';' listj {printf(GREEN("%d "),$1);}
;
%%

Une spécification complète qui couvre l'ensemble des questions précédentes : list.bison


2. GaBuZoMeu, ou langage de Dyck

Issu du TP Noté 2013.
Le peuple shadok ne connaît que quatre syllabes : "Ga", "Bu", "Zo" et "Meu".
Tout les mots de la langue shadok sont obtenus par concaténation de ces syllabes fondamentales. Par exemple selon Wikipedia : "ZoGa" signifie pomper, "ZoBuGa" signifie pomper avec une petite pompe, et "ZoBuBuGa" signifie pomper avec une grosse pompe.

2.0) Mots "MeuMeu".

Le professeur Shadoko a défini la famille des mots "MeuMeu" comme suit :

Le lecteur vérifiera en exercice les exemples suivants : (gabu.data)

2.1) Travail demandé

  1. Décrire la famille des mots MeuMeu grâce à une grammaire context-free non ambiguë.
  2. Écrire un analyseur (syntaxique+lexical) permettant de reconnaître les mots MeuMeu écrits ligne à ligne. (on pourra utiliser l'option Flex %option case-insensitive)

Corrigé:

2.2) Indication et complément

Le peuple Gibi, ennemi des shadoks, semble avoir un ensemble de mots comparable, mais à la place des syllabes Ga, Bu, Zo et Meu, ils utilisent des symboles ésotériques : {, [, ], }.

En fait le langage "MeuMeu" est connu usuellement comme langage des mots bien parenthésés, ou en informatique théorique comme langage de Dyck ici sur 2 paires de parenthèses. Ce langage joue un rôle particulier en informatique théorique dans la mesure où il apparaît comme l'exemple générique ou générateur de tout les langages algébriques :
Soit Dn, le langage de Dyck sur n paires de parenthèses, on a les énoncés équivalents du théorème de Chomsky-Schützenberger :


3. Calculatrice Arithmétique

Cet exercice illustre l'utilisation combinée de Flex et de Bison avec les notions :

3.1) Calculer avec des entiers sans ambiguïté

Il s'agit de calculer ou d'évaluer des expressions bien parenthésées, ne comportant que des valeurs numériques et les opérateurs arithmétiques usuels.

Dans un premier temps, on suppose :

Définir les catégories lexicales nécessaires, définir le passage de valeurs pour ces catégories lexicales, écrire l'analyseur lexicale, définir la grammaire et écrire l'analyseur syntaxique.

Corrigé : calc0.flex et calc0.bison

3.2) Calculer avec des entiers et la priorité des opérateurs

On désire maintenant accepter les expressions arithmétiques avec les règles usuelles de priorité et d'associativité des opérateurs.

Modifier la grammaire pour avoir une grammaire simple et éventuellement ambiguë.
Ajouter une gestion des priorités pour résoudre les conflits avec les directives : %left %right %nonassoc

1+2*3             ==7
1*2+3             ==5
7-3-2-1           ==1
6/3/2             ==1

Corrigé : calc1.flex et calc1.bison

3.3) Calculer avec des réels

Modifier la calculatrice pour accepter des opérations flottantes ou entières.
N.B. : la gestion de "cast" sur les types peut être un peu lourd. On veut ici garder le type entier pour l'opération modulo "%"; Pour les autres opérations on transformera rapidement les entiers en flottant. L'expression "4 % (1+1)" sera ainsi invalide...

Corrigé : calc2.flex et calc2.bison

3.4) Calculer avec des variables, utilisation d'une table de symbole rudimentaire

Les expressions peuvent maintenant contenir des noms de variables. L'affectation d'une valeur se fera sous la forme "nom = Expr \n". L'utilisation d'une variable à l'intérieur d'une expression s'obtient directement par son "nom".

A= 1 + 2 * 3    // A<-7
A               // == 7 
B= 3 * A - 1    // B<-20
B+5             // ==25
A*A-B*B-(A+B)*(A-B) // ==0 !

Les noms de variables seront uniquement les 26 lettres majuscules A,B,..Z. On pourra alors implémenter une table des symboles simple par un tableau fixe de taille 26, servant à stocker la valeur de chaque variable, ou le fait qu'une variable soit initialisée.
N.B. : En C, pour un caractère "char c" dans [A-Z], " c - 'A' " donne un index entier entre 0 et 25. Réciproquement, pour un entier "int i" entre 0 et 25 " 'A' + i " donne les caractères entre A et Z.

Modifier la calculette pour gérer les nom de variables.

NB: On suggère dans un premier temps de ne considérer que des affectations simples sur des lignes spécifiques. On peut ensuite généraliser en considérant une affectation comme une expression et permettre comme en C des affectations multiples "a=b=2", ou des affectations internes "a=3*(b=2+(c=3))". On notera que le "=" devient ainsi un opérateur associatif à droite.

Corrigé : calc3.flex et calc3.bison

3.5) Récupération des erreurs

Prévoir des cas de récupérations d'erreurs:

Ajouter aussi des contrôles d'erreurs sémantiques comme par exemple "utilisation de variable non initialisée", "Réaffectation de variable déjà initialisée" ...

Corrigé : Cf. corrigé de la question suivante ( calc4.flex et calc4.bison)

3.6) Traitement du '-' unaire

Améliorer la calculette en autorisant l'utilisation du '-' unaire.
Ajouter éventuellement la puissance '^' (en C : double pow(double x, double y) avec #include <math.h>, et gcc -lm )

La calculette est elle correcte ? :

  1. Tracer comment est évaluée l'expression "-2*3"; est-ce "(-2)*3" ou "-(2*3)" ?
  2. Tester "6/-3/2" ? idem "1*-2*3" ?
  3. Comment traiter "-3^2" : "(-3)^2" ou "-(3^2)" ?

Le problème est que la priorité définie pour le token "-" binaire, ne convient pas pour le '-' unaire.
Bison dispose d'un mécanisme pour ajouter des priorités spécifiques au niveau d'une règle de grammaire. Pour cela, il faut définir dans le préambule de la spécification un token fictif par exemple %left UMINUS. La position de ce token par rapport aux autres directives "%letf %right %nonassoc" fixe sa priorité. Ensuite, pour une règle où l'on veut une priorité spécifique, ajouter à la fin de la règle (avant l'action) la directive %prec UMINUS

Tester l'analyseur en fixant une priorité spécifique pour le moins unaire. Vérifier le comportement dans les différents cas :

Corrigé : calc4.flex et calc4.bison
NB: Le fichier Makefile générique est modifié pour prendre en compte la fonction pow avec l'ajout d'une ligne "calc4 : EXT_SRC = -lm"

Notes Complémentaires sur la priorité du '-' unaire ?

  1. Pour l'exemple "-2*3" on peut se dire que c'est indifférent puisque "(-X)*Y==-(X*Y)", sauf que cette identité mathématique n'est plus vraie si les variables mathématiques deviennent des variables informatiques.
    Exemple :
    ------ TestEval.c 
    #include <stdio.h>
    int main() {
        int a=1; printf(" -(++a*++a) =  %d\n", -(++a*++a));
            a=1; printf(" (-++a)*++a =  %d\n", (-++a)*++a);
            a=1; printf(" -++a*++a   =  %d\n", -++a*++a);
        return 0;
    }
    

    --TestEval.resu
     -(++a*++a) =  -9
     (-++a)*++a =  -6
     -++a*++a   =  -6
    

    Ainsi en C, mais aussi en général, le moins unaire est plus prioritaire que la multiplication.
  2. Pour une division "6/-3/2", on voit que la priorité du moins unaire peut casser l'associativité gauche de la division ("%left '/'"). Si le '-' unaire n'est pas prioritaire, l'analyseur ne trouve pas ici de conflits "Expr / Expr / Expr" car il aura réduit le "-(3/2)" avant.
    Il convient donc que le '-' unaire soit prioritaire sur la division.
    Dans le cas de la multiplication c'est moins grave (pour le mathématicien!) car la multiplication est complètement associative.
  3. Il semblerait que le résultat le plus naturel soit "-3^2 = - (3^2) ", c'est-à-dire que le moins unaire soit moins prioritaire que la puissance.
    Pourtant beaucoup d'outils informatiques utilise un '-' unaire plus prioritaire que la puissance.
    Par exemple, la commande Unix "bc" qui est très proche de notre calculette donne "-2^2 == 4".
    Ceci résulte d'un usage réducteur consistant à rendre les opérateurs unaires "globalement" prioritaires sur les opérateurs binaires.

3.7) Calculer avec des fonctions

Ajouter à la calculette des fonctions arithmétiques avec une syntaxe usuelle "Nom_Fonction(exp1 , exp2 , ...)".

  1. Ajouter une fonction maximum Max() (ou min(), ou somme(), ou prod(),... ) à deux arguments.
    Ex : 5 + Max(3+2, 4) * 2 =>15
  2. Ajouter une fonction minimum Min() (ou autre..) à N>0 arguments.
    Ex : 5 + Min(3+2, Min(3,1)*2 , 2.5 , 5 ) * 2 =>9

NB: En C, l'ordre d'évaluation des arguments d'une fonction est indéterminé, on pourra donc choisir ici ce qui convient le mieux à Bison (Récursivité gauche)

Corrigé : calc5.flex et calc5.bison. La fonction Min() est définie avec N>0 arguments, la fonction Max() est définie avec deux arguments.
NB: Le fichier Makefile générique est modifié pour prendre en compte la fonction pow avec l'ajout d'une ligne "calc5 : EXT_SRC = -lm"

3.8) Utilisation d'une table de symbole - Corrigé

A titre d'illustration de la notion de table de symboles, on donne ici une version étendue de la calculatrice arithmétique qui autorise comme nom de variable des identifiants quelconques.

On se donne une implémentation de base de la structure algorithmique table de symboles avec les fichiers symtab.c et symtab.h (ou Pretty Print HTML des 2 fichiers). La structure "_stab" dans "symtab.h" pourra être adaptée pour enregistrer les différentes informations utiles que l'on veut associer à chaque identificateur de notre langage.

Pour chaque identificateur rencontré au niveau lexical :

L'adaptation de la version finale "calc5" du corrigé de la calculatrice donne l'analyseur calc-symtab.flex et calc-symtab.bison.
NB: Pour tester, le fichier Makefile générique est modifié avec l'ajout d'une ligne "calc-symtab : EXT_SRC = symtab.c -lm"


4. Arbres de Syntaxe Abstraite : Calculatrice vs. Compilateur

Dans une analyse syntaxique de type "calculette", il n'y a pas de construction explicite de l'arbre de syntaxe décrivant l'analyse mais une évaluation au fur et à mesure de la construction de l'arbre.
On veut illustrer ici, l'approche opposée qui consiste à construire explicitement l'arbre de syntaxe abstraite pour un traitement ultérieure au niveau de l'analyse sémantique, ou de la génération de code.

4.1) Travail demandé

Transformer la "calculette" arithmétique de l'exercice précèdent, de façon à construire un "compilateur" d'expression arithmétique, c'est à dire de produire explicitement et d'imprimer l'arbre de syntaxe abstraite.

4.2) Indications

Sur le principe, l'exercice est simple puisqu'il suffit juste (!) :

Dans la pratique, ce qui peut être un peu pénible, c'est la gestion des feuilles qui outre la catégorie lexicale contiennent éventuellement une valeur issue de l'analyse lexicale (valeur entière, flottante, référence dans une table de symboles...). Pour alléger l'exercice, on oublie ces valeurs et l'on considère des arbres syntaxiques ne contenant qu'une chaîne de caractère pour chaque noeud. La chaîne est le nom de la règle pour un symbole non-terminal (et donc un noeud interne) ou un nom de token pour une feuille.

On se donne une implémentation de structure d'arbres N-aires avec les fichiers arbre.c et arbre.h (ou Pretty Print HTML des 2 fichiers)

Usage :

Exemple d'utilisation pour l'exercice:

%union {
 ...
 arbre ARBRE
}
%token <ARBRE> Expr
%%
 Expr : Expr '+' Expr  { $$ = N("regle plus", $1, F("+"), $3); }
%%

Exemple de résultat avec l'impression print_arbre_height()

-3^4*2


 -      int    ^      int    *      int
 |      |      |      |      |      |
 |      ---------------      |      |
 |             |             |      |
 |             R^            |      |
 |             |             |      |
 |             ----------------------
 |                        |
 |                        R*
 |                        |
 --------------------------
             |
             Rumin

Corrigé : calc-AST.flex et calc-AST.bison.
Et un fichier Makefile modifié avec l'ajout d'une ligne "calc-AST : EXT_SRC = arbre.c"


5. Encore des calculettes (Yet Another CalC!)

Deux variations autour de l'exercice "Calculatrice Arithmétiques" :

5.1) Calculatrice logique d'ordre 0

cf. TP Noté 2013 Exercice 1 "Analyse syntaxique d'expressions booléennes" Sujet+Corrigé

5.2) Calculatrice Ensembliste

Avec deux versions de grammaire au choix :

cf. TP Noté 2014 Exercice 3 "Analyse syntaxique d'expressions ensemblistes" Sujet+Corrigé


6. Passage de Chaînes de caractères entre Flex et Bison - Corrigé

On donne ici une illustration du passage de valeurs de type chaîne de caractères entre Flex et Bison. Le choix est soit de faire une recopie systématique des chaînes (et de faire les free après usage), soit d'utiliser un pointeur en supposant que le buffer pour yytext est commun pour les différents fragments.

Le second choix semble fonctionner dans la pratique mais n'est spécifié nulle part et peut donc être dépendant de l'implémentation. Il est d'autre part sujet à des effets de bord dans le cas d'utilisation des fonctions flex comme unput(), yyless(), yymore().

Le premier choix avec par exemple l'utilisation de la fonction strdup() est donc en général préférable.


7. Titre du WallStreetJournal

Le but est d'explorer les possibilités d'analyse syntaxique en langage naturel, sans toutefois rentrer trop dans la linguistique. On acceptera donc des solutions empiriques. L'exemple proposé est celui des sous-titres du Wall Street Journal, esquissé dans les slides du cours et dans l'exercice "Premier pas Bison". Il s'agit à chaque étape, d'améliorer la grammaire pour accepter les titres (réels) donnés en exemple.

7.1 "Verizon, Bell South won't cut DSL bills ."

  1. Verizon will cut the bills .
    Définir une classe lexicale AUX, et une règle syntaxique TransVerbGroup représentant un TRANS_VERB seul, ou éventuellement précédé d'un auxiliaire
  2. Verizon won't cut the bills .
    Une solution rustique est d'enrichir la classe lexicale AUX
  3. Verizon won't cut DSL bills .
    Reconnaître "DSL" comme un COMM_NOUN.
    Définir une CNounList comme étant une liste de 1 ou plusieurs COMM_NOUNs.
    L'utiliser à la place de COMM_NOUN dans la ou les règles adéquates.
  4. Verizon, Bell South won't cut DSL bills.
    Ajouter "South" aux PROP_NOUN possibles.
    Définir PropNouns comme étant une liste de 1 ou plusieurs PROP_NOUNs.
    Définir COMMA comme ','.
    Élargir la définition de NounGroup a la reconnaissance de un ou plusieurs PropNouns sépares par des COMMAs.

7.2 "Daimler ordered to reimburse ex-holders ."

7.3 " stocks decline on oil again ."


8. Langage naturel élémentaire, construction de structures

Le but est de créer des structures mémorisant l'analyse syntaxique d'une phrase pour des analyses sémantiques ultérieures plus poussées. On se contente ici d'une structure de phrase très simple, pour éviter des analyses linguistiques spécialisées :

----  langnat.data
une grande entreprise rachete une petite PME .
les petites PME informatique survolent un marche terne .

le chat gris attrappe la souris verte
les vertes souris mangent les chats gris
une jolie souris attrappe le gentil chat
les petits chats jolis attrappent les souris vertes
les jolis chats marrants attrappent les gentilles souris
le chat court vite
si le joli chat attrappe la souris , alors le chat mange la souris

On se donne une version de départ (correspond au 1er exemple): langnat.flex et langnat.bison

8.1 Reconnaisseur seul

Vous pouvez compléter la grammaire pour traiter quelques autres exemples à votre idée. Pour traiter en une seule fois plusieurs Phrases, penser à définir une notion de Texte. et traiter les erreurs.

8.2 Passage des valeurs de "token"

Pour remonter les valeurs des "tokens" (chaînes de caractères) vers le niveau syntaxique et sémantique, il convient de recopier les valeurs de yytext et de remonter dans yylval un pointeur sur cette copie. (cf 6. Passage de Chaînes de caractères)

8.3 Construction de la structure d'une phrase

Il s'agit de créer des structures de données pour mémoriser les éléments d'une phrase. Les structures et fonctions associées seront programmées dans un fichier séparé à inclure au niveau du prologue (mettre au point ce code d'abord isolement).

Un exemple de code de départ est donné dans langnat.c pour une structure "GroupNom".

NB: Pour une intégration rapide du code externe "langnat.c" dans la spécification, on se permettra de faire directement un #include "langnat.c" dans la spécification Bison (horreur!!).
La bonne pratique serait de structurer le code externe avec un "langnat.c" et un "langnat.h", de faire le #include "langnat.h" dans la spécification et d'ajouter à la compilation le "langnat.c". Avec le Makefile générique du cours, il suffirait d'ajouter dans le fichier Makefile une ligne "langnat : EXT_SRC = langnat.c"

Corrigé : langnat1.flex, langnat1.bison et langnat1.c
Avec par exemple en entrée :

le chat gris attrappe la souris verte
les vertes souris mangent les chats gris

CSC 4508, TELECOM SudParis, P. Hennequin, F. Meunier, F. Silber-Chaussumier
Last modified: Juillet 2015