Vous êtes sur la page 1sur 8

Support TP 3 : Analyse syntaxique avec Bison

Une fois le processus d’analyse lexicale terminé sans erreurs, le compilateur


passera à l’analyse syntaxique en utilisant la grammaire du langage auquel il est
associé ainsi que la suite des entités lexicales issue de l’étape précédente.

Dans ce TP nous allons voir comment définir un analyseur syntaxique en


utilisant l’outil Bison en illustrant avec quelques exemples.

Analyse syntaxique

L’analyse syntaxique consiste à vérifier si les éléments de la suite des entités


lexicales sont dans l’ordre décrit par la grammaire du langage auquel est
associé le compilateur. En plus de cette tâche l’analyseur syntaxique doit bien
sûr signaler les erreurs syntaxiques lorsqu’elles surviennent.

Dans un analyseur syntaxique Bison nous devons donc retrouver la grammaire


du langage dans lequel est écrit le code que nous devons analyser.

Un analyseur syntaxique Bison est un fichier avec l’extension .y, une fois écrit
nous le compilerons à l’aide de la commande bison -d. Cela va nous générer un
descripteur de notre analyseur syntaxique écrit en C, qui une fois compilé va
créer l’exécutable de notre analyseur syntaxique.

Il est constitué de trois sections tout comme l’analyseur lexical Flex. Pour
séparer deux sections différente nous utiliserons %%

1. La première section est réservée aux définitions et elle est composée de


deux parties :
a. Dans la première, nous définissons les variables C dont nous
aurons besoin. Si nous devons utiliser des bibliothèques externes
dans notre code nous devons écrire les instructions qui nous
permettent de les inclure dans cette partie. Toutes ces instructions
doivent être écrites entre %{ et %}.
b. Dans la deuxième partie nous allons lister toutes les entités
lexicales (tokens) acceptés par le langage et que nous avons
définies dans l’analyseur lexical, ces entités serviront comme
terminaux dans la grammaire que nous allons définir.
Cela se fait de la manière suivante :
%token Code_Entité1 Code_Entité2 … Code_EntitéN
2. Dans la deuxième section, nous allons définir les règles de la grammaire
du langage. Chaque règle doit s’écrire sur une ligne différente et doit
être suivi de ; sur une nouvelle ligne. Il est aussi possible d’associer à
chaque règle, partie de règle ou élément d’une règle (terminal, non
terminal) une ou plusieurs action en C écrit entre accolade {} mais cela
est principalement utilisé dans la partie sémantique.
3. Enfin, la troisième section permet de lancer l’analyseur syntaxique en
appelant la fonction prédéfinie yyparse() à l’intérieur du main. Dans
cette partie nous pouvons aussi redéfinir les fonctions prédéfinies de
Bison cela doit se faire avant le main. La fonction la plus utilisée que
nous pouvons modifier est la fonction yyerror(), fonction qui permet de
gérer les actions que nous souhaitons lancer lorsque nous rencontrerons
des erreurs syntaxiques.

Pour résumer, la vue d’ensemble de la structure que nous venons de décrire


est comme suit :

%{
Définition des variables C
%}
%token liste des tokens
%%
Règles de la grammaire
%%
Redéfinition des fonctions de Bison
Lancement de l’analyseur

Exemples :
Pour les exemples suivants nous allons utiliser le même langage que nous
avons défini pour l’analyse lexicale.
Dans le premier exemple nous allons définir un analyseur syntaxique qui vérifie
uniquement l’instruction d’affichage printf, cette instruction ne s’exécutera
qu’une seul fois. Le code Bison correspondant est donné comme suit :

%token Mc_Print IDF STR PV Par_Ouv Par_Fer err


%%
S:Mc_Print Par_Ouv MessAffich Par_Fer PV
;
MessAffich:STR|IDF
;
%%
int main()
{
yyparse();
return 0;
}

Dans cet exemple, vu que nous n’avons pas eu besoin de variable la première
partie de la première section a été omise, l’ensemble des tokens a été listé
juste après le mot clé %token.

Dans la deuxième section, la grammaire a été définie en introduisant les


différentes règles (deux dans notre cas). Veuillez bien noter la forme d’écriture
d’une règle.

S : Mc_Print Par_Ouv MessAffich Par_Fer PV


;

En vert sont représentés les terminaux, en bleu les non terminaux et en rouge
le membre gauche (l’axiome dans notre cas). Le : sert à séparer la partie
gauche et la partie droite (équivalent de la flèche) et le ; marque la fin d’une
règle.
Et dans la dernière section nous faisons appel à la fonction yyparse pour lancer
l’analyseur syntaxique, dans cet exemple nous n’avons eu besoin de redéfinir
de Bison.
Dans le deuxième exemple, nous accepterons l’écriture de la fonction printf
plusieurs fois dans le code à analyser.
Le code Bison correspondant est le suivant :

%token Mc_Print IDF STR PV Par_Ouv Par_Fer err


%%
S:PlsAffich
;
PlsAffich: Mc_Print Par_Ouv MessAffich Par_Fer PV PlsAffich|
;
MessAffich:STR|IDF
;
%%
int main()
{
yyparse();
return 0;
}

La différence entre les deux codes présentés réside dans la partie des règles de
production. En effet dans le deuxième code nous avons ajouté une nouvelle
règle récursive qui nous permet d’accepter de répéter l’exécution de
l’instruction printf. Pour représenter l’epsilon nous n’écrivons rien après la
barre.

Dans le dernier exemple nous allons définir l’analyseur syntaxique qui


correspond cette fois-ci à toutes les instructions du langage Lp. Le code source
qui lui correspond est donné comme suit :

%{
extern ligne;
int nb_err_syn=0;
extern nb_err_lex;
extern yytext;
%}
%token Mc_Int Mc_For Mc_Print IDF STR Affect PV Par_Ouv Par_Fer CST_Int
Opr_Inf Opr_Plus err
%%
S:PlsDec PlsInst
;
PlsDec:Dec PlsDec|
;
Dec:Mc_Int IDF Affect CST_Int PV
;
PlsInst: Inst PlsInst |
;
Inst : InstFor InstAffich|InstAffich
;
InstFor:Mc_For Par_Ouv InitFor CondFor IDF Opr_Plus Opr_Plus Par_Fer
;
InitFor:IDF Affect CST_Int PV
;
CondFor:IDF Opr_Inf CST_Int PV
;
InstAffich:Mc_Print Par_Ouv MessAffich Par_Fer PV
;
MessAffich:STR|IDF
;
%%
yyerror()
{
nb_err_syn++;
printf("erreur syntaxique a la ligne %d generee par %s\n",ligne,yytext);
return 1;
}
int main()
{
yyparse();
if (nb_err_syn==0 && nb_err_lex==0)
printf("Syntaxe correcte");
else
printf("Syntaxe incorrecte");
return 0;
}

Avant de décrire ce qui a été ajouté dans le troisième code par rapport aux
deux premiers, certains points importants doivent être expliqués.
Comme nous l’avons mentionné ci-haut, l’analyseur syntaxique aura comme
input la suite des entités lexicales issue de l’étape précédente. Par conséquent
l’analyseur lexical que nous avons déjà conçu doit retourner cette suite de
tokens à l’analyseur syntaxique. Ce qui fait que nous devons effectuer des
changements dans le code source qui défini notre analyseur lexical afin de
pouvoir faire le lien entre les deux analyseurs. Mais surtout, il faut compiler les
deux fichiers .c correspondants (celui de l’analyseur lexical et celui de
l’analyseur syntaxique) à la fois en seule ligne de commande comme nous
allons le voir à la fin de ce document.
La première chose à modifier dans le code de l’analyseur lexical que nous avons
déjà écrit est la deuxième section. En effet, l’ajout des noms des entités
lexicales à la chaine de caractère avait pour but de simuler le passage de la
suite d’entités lexicales à l’analyseur syntaxique ce qui n’est plus pertinent vu
que cette fois-ci nous allons effectuer l’analyse syntaxique. Ainsi nous
remplacerons chaque instruction strcat(NomChaine,NomEntite) par
l’instruction return NomEntite. Il faut bien notez que ces noms d’entités que
nous allons retourner sont les mêmes qui doivent se trouver dans la partie
%token dans l’analyseur syntaxique. Pour pouvoir faire cela il faut ajouter le
nom du fichier contant la description de l’analyseur syntaxique suivi d’un
.tab.h, comme suit : #include "nomFichier.tab.h" dans la première partie de la
première section.
La deuxième chose à modifier, c’est d’enlever la fonction main de l’analyseur
lexical. En effet lors de l’analyse syntaxique, la fonction yyparse que nous avons
décrite ci-dessus (qui est dans le main de l’analyseur syntaxique) lancera
l’analyseur lexical et si nous n’enlevons pas le main de l’analyseur lexical nous
aurons deux main dans notre programme ce qui provoquera des erreurs.

Intéressons nous maintenant au code de l’analyseur syntaxique. Dans la


première partie de la première section, nous avons déclaré la variable
nb_err_syn pour compter le nombre d’erreurs syntaxiques, cette variable nous
servira à la fin de l’analyse pour valider ou non le code analysé. Il est bien de
noter que cela est optionnel, car vous pouvez valider l’input au niveau de la
première règle en ajoutant l’instruction {printf("syntaxe correcte");} (ou
n’importe quel autre message) avant le point virgule.
Comme nous l’avons mentionné l’analyseur syntaxique doit signaler l’erreur
syntaxique en précisant la ligne sur laquelle elle survient, or c’est l’analyseur
lexical qui s’occupe de la gestion de lignes. Nous avons ainsi besoin de passer
cette information de l’analyseur lexical à l’analyseur syntaxique, pour faire cela
il suffit d’écrire l’instruction extern ligne ; dans la section dédiée à cela. Cela
signifie que nous somme entrain de partager une variable entre deux fichiers.
Et nous pouvons faire la même chose pour la variable yytext (extern yytext ; )
La suite de la première section sert juste à lister les noms des tokens que nous
avons retournés dans la partie analyse lexicale (il est préférable de faire des
copier-coller pour éviter toute erreur possible).

Dans La deuxième section nous avons ajouté d’autres règles afin d’enrichir la
grammaire.

Pour finir, nous avons redéfini la fonction yyerror pour modifier le message
d’erreur syntaxique et bien préciser la ligne sur laquelle apparait l’erreur, nous
incrémentons aussi la variable nb_err_syn à ce niveau. Mais ce traitement
reste facultatif et vous pouvez ne pas le faire.

Une fois que l’analyseur syntaxique est écrit, nous devons le compiler en
utilisant la commande bison.
Pour cela il faut ouvrir l’invité de commande là ou se trouve le fichier contenant
votre analyseur syntaxique et taper :
bison -d nom_fichier.y
Cette commande va nous générer deux fichiers le premier est nommé :
nom_fichier.tab.c, c’est ce fichier qui sera compilé afin de lancer l’analyse
syntaxique et le deuxième nommé: nom_fichier.tab.h que nous ajouterons au
début du fichier flex. Nous devons ainsi compiler le premier fichier à l’aide de la
commande gcc, mais comme nous devons aussi lancer l’analyseur lexical nous
devons compiler le fichier qui le décrit i-e le fichier .l avec la commande flex et
compiler ensuite le fichier lex.yy.c avec la commande gcc, notez bien qu’il faut
compiler ces deux fichiers à la fois avec une seule commande comme nous
l’avons mentionné dans les paragraphes précédents:
gcc lex.yy.c nom_fichier.tab.c -lfl -ly

Notons que lfl et ly sont des bibliothèque de bison que nous devons utiliser
pour que nous puissions compiler le fichier nom_fichier.tab.c en raison de
l’utilisation de certains fonctions appartenant à ces bibliothèque et que le
compilateur gcc ne connait pas.
Par défaut le nom du fichier exécutable générée sera nommé a.exe, si vous
voulez changer ce nom veuillez ajouter à la fin de la commande précédente ce
qui suit : -o nom_ fichier.exe.
Pour tester votre analyseur syntaxique, vous aller soit le lancer en tapant le
nom de l’exécutable dans l’invité de commande et ensuite saisir le code que
vous voulez tester manuellement. Soit lui passer en paramètre un fichier .txt
contenant le code à analyser en utilisant la commande suivante :
nom_ fichier.exe<nom_fichier.txt

Notons qu’il est préférable de regrouper toute les instructions que vous allez
utiliser dans l’invité de commande, dans un seul fichier .bat et exécuter ce
fichier à chaque fois que vous aurez besoin de recompiler votre analyseur
syntaxique. Cela vous évitera de devoir écrire ces commandes plusieurs fois et
toutes les erreurs qui vont avec.

Contenu du fichier .bat


flex nom_fichier.l
bison -d nom_fichier.y
gcc lex.yy.c nom_fichier.tab.c -lfl –ly (-o nom_ fichier.exe optionnel)
fichier.exe ou bien nom_ fichier.exe<nom_fichier.txt

BON COURAGE

Vous aimerez peut-être aussi