Vous êtes sur la page 1sur 9

Support TP 2 : Analyse lexicale avec Flex

Introduction
Un compilateur est un programme qui prend en entrée un code source écrit
généralement dans un langage de programmation haut niveau et le
transforme en un code objet, ce dernier permettant de générer un
programme exécutable.

Le processus décrit ci-dessus comporte plusieurs étapes qui sont


essentiellement: l’analyse lexicale, l’analyse syntaxique, l’analyse sémantique
ainsi que l’optimisation et la génération de code.

L’objectif du TP est par conséquent, d’implémenter un mini compilateur dont


les principales fonctions sont l’analyse lexicale et l’analyse syntaxique. Pour
réaliser cela nous utiliserons les outils Flex (analyse lexicale) et Bison (analyse
syntaxique) qui nous permettrons de faciliter cette tâche.

Analyse lexicale
L’analyse lexicale représente la première étape de la compilation, elle consiste
à vérifier si les caractères/mots du code source en entrée appartiennent au
langage auquel est associé le compilateur.
Un analyseur lexical doit:
 Générer une suite d’entités lexicales (appelées tokens également) à
partir du code source lu, cette suite sera passée à l’analyseur syntaxique
afin de poursuivre le processus de compilation.
 Ignorer les parties superflues telles que les commentaires, les espaces
et les sauts de lignes …
 Gérer les numéros de lignes, cela permet d’associer à chaque erreur
rencontrée le numéro de la ligne dans laquelle elle apparue.
 Signaler les erreurs lexicales lorsqu’elles surviennent, en précisant leur
emplacement dans le code source.

Ainsi, l’analyseur lexical que nous allons concevoir doit réaliser tout ce qui a été
mentionné en supra. Nous verrons de manière plus détaillée comment cela se
fait dans les prochains paragraphes.

Afin de pouvoir générer la suite des tokens, l’analyseur lexical doit d’abord lire
le code source et n’accepter que ce qui a été décrit par les règles lexicales du
langage. Pour définir ces règles lexicales, nous utilisons des expressions
régulières qui sont l’équivalent des automates.

Une expression régulière est une suite de caractères qui permet de contrôler
les caractères d’un texte dans le but de s’assurer de sa validité. Dans une
expression régulière chaque caractère est significatif et correspond à sa valeur
littérale, y compris les espaces. Exemple : l’expression régulière hello world
correspond à la chaine de caractère hello world.
Cependant certains caractères sont réservés, et ont chacun une certaine
fonctionnalité. Pour indiquer que nous souhaitons avoir leur valeur littérale,
nous devons les précéder du caractère spéciale \ ou bien les mettre entre deux
côtes " ".

Les principaux caractères réservés sont:


 +/*/ ? : La fonctionnalité de ces trois caractères est très similaire, elle
permet de vérifier si un caractère ou un ensemble de caractères est
répété : une ou plusieurs fois pour le +, zéro ou plusieurs fois pour le * et
pour finir zéro ou une seule fois pour le ?.
 | : Représente l’union. Il permet de vérifier que le caractère en cours de
lecture est l’un des caractères séparés par ce symbole.
Exemple : a|b|c représente le caractère a ou le b ou le c.
 () : La factorisation, utiliser ces deux symbole signifie que l’opération qui
suit la parenthèse fermante est appliquée sur tous l’ensemble de
caractère entre parenthèse.
Exemple : Notez bien la différence entre ab+ qui représente la chaine de
caractère abb….b et (ab)+ qui représente la chaine de caractère
abab….ab.

 " " et \ : comme nous l’avons mentionné ci-dessus, ces deux caractères
permettent d’avoir la valeur littérale d’un caractère, cependant le
symbole \ est utilisé uniquement avant les caractères réservés ou bien
certain autres caractères comme par exemple le n dans \n pour indiquer
le saut de ligne.
Exemple : \\ représente la valeur littérale de \ et "+" représente la
valeur littérale de +.
Il faut noter que \ et plus prioritaire que ", ainsi \" nous donnera la
valeur littérale de ", mais "\ est une expression incomplète car un autre
caractère doit suivre le \ et que nous devons ajouter un autre ", pour les
même raisons "\" et incomplète également.
 . : le point indique que l’expression régulière accepte tous les caractères
possibles mis à part le saut de ligne \n.
 […] : Permet de déterminer des ensembles, cette notation indique que
l’expression régulière accepte un des caractères écrits entre les deux
crochets.
Exemple : [abc] représente le caractère a ou le b ou le c et qui
équivalente à l’expression (a|b|c).
[aeiouy]+ correspond à une suite d’un ou plusieurs caractères
appartenant chacun à l’ensemble [aeiouy] sans que ça ne soit le même
caractère sur toute la chaîne bien sûr.
 - : lorsqu’il est utilisé entre deux crochet il correspond à un ensemble de
caractères dont les valeurs sont comprise dans un intervalle, il faut
noter que le – doit être écrit entre les deux bornes de l’intervalle et que
les codes ASCII de tous les éléments de l’intervalle sont contigus.
Exemple : [0-9] correspond à l’expression (0|1|2|3|4|5|6|7|8|9)
 ^ : Lorsqu’il est utilisé entre deux crochet il correspond à la négation,
autrement dit l’expression régulière acceptera tous les caractères
hormis ceux qui viennent après le ^. Notons que le ^ doit être suivi par
au moins un autre caractère, ainsi l’expression [^] n’est pas correcte.
Exemple : [^ab] signifie que nous allons accepter tous les caractères à
part le a et le b.
Note : à l’intérieur des crochets tous les caractères correspondent à leur
valeurs littérales sauf le \ le – s’il est compris entre deux valeurs et le ^
s’il est suivi de caractères. Exemple : [.+] veut dire que l’expression
accepte un caractère dont la valeur est soit le point soit le plus (valeurs
littérales) et non pas une suite d’un ou plusieurs caractères acceptant
tous les caractères sauf le saut de ligne.

Les expressions suivantes sont des exemples d’expressions régulières utilisées


par les analyseurs lexicaux pour définir leurs règles lexicales :
Identificateur : Nous le représentons en général par une suite de caractères
alphanumériques qui commence par un caractère alphabétique, le tiret
underscore _ peut être accepté également nous l’inclurons aussi dans
l’expression qui sera donnée par : [a-zA-Z][a-zA-Z0-9_]*. Nous avons utilisé
l’étoile et non pas le plus car un nom d’identificateur peut être représenté par
un seul caractère.
Chaine de caractère : Elle est représentée par une suite de caractères
écrite entre deux côtes, dans une chaine de caractère nous acceptons tous les
caractères existant mis à part le saut ligne. Nous pouvons ainsi écrire ce qui
suit : \".*\" ou bien ["].*["], * est utilisée au lieu du + pour accepter les
chaînes vides.
Constante numériques : Considérons dans cet exemple uniquement les
valeurs entières, l’expression correspondante est : [1-9][0-9]*|0 pour dire
que les zéros à gauche ne sont pas acceptés lorsqu’il y en a plus qu’un.

Comme nous l’avons mentionné dans les paragraphes précédents, les


expressions régulières sont utilisées dans la définition des règles lexicales d’un
langage. Dans ce TP, cette étape de la compilation est effectuée à l’aide de
Flex qui est un outil dédié à cela comme nous l’avons vu précédemment, il
nous permet de définir des analyseurs lexicaux écrit dans un langage qui lui
est associé.

Un analyseur lexical Flex est donc un fichier dont l’extension est .l, dans lequel
nous décrivons les règles lexicales du langage à analyser ainsi que les actions
associées à ces règles. Dans les paragraphes suivants nous présenterons la
structure générale d’un fichier .l ainsi qu’un exemple d’analyseur lexical
simple.

Un fichier .l est constitué de trois sections qui sont séparé par %%


1. La première section est réservée aux définitions, elle comporte deux
parties:
o Dans la première nous définissons les variables C dont nous aurons besoin
telle que la variable qui gèrera le numéro de lignes, 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 %}.
o La deuxième partie de la première section est consacrée aux définitions des
expressions régulières. Chaque définition est sous la forme :
Nom_Expression expression régulière
Notons que chaque expression régulière doit s’écrire sur une ligne
différente.
2. Dans la deuxième section du fichier, nous allons associer à chaque
expression régulière une action en C. La gestion des erreurs se fait dans
cette partie également. De même chaque paire Expression Action doit
s’écrire sur une ligne différente.
3. Enfin, la troisième section permet de lancer l’analyseur lexicale à l’aide
de la fonction prédéfinie yylex() mais aussi de redéfinir les fonctions de
Flex existante déjà.

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


est comme suit :
%{
Définition des variables C
%}
Définition des expressions régulières
%%
{Expression régulière} {Action C}
%%
Redéfinition des fonctions de Flex
Lancement de l’analyseur
Exemple :
Soit Lp un langage de programmation basique dont les fonctions se limite à :
-La création et l’initialisation de variables de type entier uniquement :
Ex : int n = 10 ;
-L’affichage du contenu d’une variable de type entier ou d’un message sous
forme de chaine de caractère :
Ex : printf(n) ; ou bien printf("message a afficher") ;
-L’utilisation de la boucle for de la manière suivante :
Ex : for (int i=0 ;i<n ;i++)
Bien sûr La seule instruction permise à l’intérieur de la boucle est la fonction
d’affichage.
Par conséquent les différentes entités lexicales que nous devons avoir sont :

Mc_Int Mc_Print Mc_For IDF Affect PV Par_Ouv Par_Fer


Opr_Plus Opr_Inf STR CST_Int

(Vous pouvez bien évidemment les nommer différemment)

Dans un premier temps, étant donné que nous n’allons effectuer que l’analyse
lexicale, nous simulerons le résultat de cette étape qui est la suite des entités
lexicales en créant une chaine de caractère qui sera initialement vide et à
laquelle nous ajouterons les noms des entités lexicales au fur et à mesure que
nous les rencontrions. A la fin si aucune erreur lexicale n’a été détectée, la
chaîne représentant les tokens sera affichée et nous indiquerons que le code
est lexicalement correct.

Dans ce qui suit, nous allons écrire l’analyseur lexicale associé au langage Lp
que nous venons de définir. Le code Flex correspondant est le suivant :

%{
#include <string.h>
int ligne=1;
char Entite_Lexicale []="";
int nb_err_lex=0;
%}
Mc_Int int
Mc_Print printf
Mc_For for
STR ["].*["]
IDF [a-zA-Z][a-zA-Z0-9_]*
Affect =
PV ;
Par_Ouv \(
Par_Fer \)
CST_Int [1-9][0-9]*|0
Opr_Plus \+
Opr_Inf <
%%
{Mc_Int} strcat(Entite_Lexicale,"Mc_Int ");
{Mc_Print} strcat(Entite_Lexicale,"Mc_Print ");
{Mc_For} strcat(Entite_Lexicale,"Mc_For ");
{STR} strcat(Entite_Lexicale,"STR ");
{IDF} strcat(Entite_Lexicale,"IDF ");
{Affect} strcat(Entite_Lexicale,"Affect ");
{PV} strcat(Entite_Lexicale,"PV ");
{Par_Ouv} strcat(Entite_Lexicale,"Par_Ouv ");
{Par_Fer} strcat(Entite_Lexicale,"Par_Fer ");
{CST_Int} strcat(Entite_Lexicale,"CST_Int ");
{Opr_Plus} strcat(Entite_Lexicale,"Opr_Plus ");
{Opr_Inf} strcat(Entite_Lexicale,"Opr_Inf ");
[ \t]
[\n] ligne++;
. {nb_err_lex++;printf("erreur lexicale a la ligne %d generee par %s\n",ligne,yytext);}
%%
int yywrap(){}
int main()
{
yylex();
if (nb_err_lex==0)
{
printf("Code lexicalement correct \n");
printf("Tokens : %s\n",Entite_Lexicale);
}
return 1;
}

Pour certains compilateurs il est nécessaire d’ajouter les bibliothèques


<stdio.h> et <stdlib.h> pour que ce code fonctionne.
-Dans la partie déclarations et définitions de variables C, la bibliothèque
string.h a été importé pour pouvoir utiliser les fonctions prédéfini sur les
chaines de caractères. La variable ligne est utilisée pour gérer le numéro de
lignes, elle sera incrémentée à chaque saut de ligne et la variable nb_err_lex
est utilisée pour compter le nombre d’erreurs lexicales.
Les définitions des expressions régulières sont écrites dans la deuxième partie
de la première section.

-Dans le deuxième section, nous associons à chaque expression régulière une


ou plusieurs action en C. Notons que le nom de l’expression régulière doit être
mis entre deux accolades, quant aux actions, s’il y en a plus d’une nous devons
les mettre entre deux accolades. Il faut noter que lorsque une expression
régulière e1 est incluse dans une autre expression e2 il faut écrire e1 avant e2
Sinon l’analyseur reconnaitra les entités définies par e1 comme des entités de
e2 parce que il va lire les expressions une par une et dès qu’une expression est
vérifiée il passera à une autre entité sans continuer de vérifier pour le reste des
expressions. Par exemple : les expressions des mots clés sont incluses dans la
définition d’un identificateur (IDF suite de caractères alphanumérique et for
par exemple est une suite de caractères alphanumérique).
Remarque :
Pour les espaces et les sauts de lignes nous ne les avons pas écrits dans la
partie définition mais nous leur avons associé des actions en C. En réalité nous
pouvions écrire toutes les définitions directement dans la deuxième section et
leur associer des actions. Cependant par convention nous les avons écrites de
la manière que nous venons de présenter.
Pour finir, le point représente en général tous les caractères possibles sauf le
saut de ligne. Cependant mis ici à la fin de tous les autres règles cela est
interprété comme étant la négation de ce qui a été défini, autrement dit tous
les caractères possible à part ce qui a été définis en dessus ce qui correspond
aux caractères non acceptés par ce langage = erreurs lexicales. Lorsqu’une
erreur lexicale survient nous incrémentons la variable nb_err_lex et nous
affichons un message d’erreur indiquons le numéro de ligne et l’entité qui a
généré l’erreur la variable prédéfini de Flex : yytext nous permet de récupérer
le.s caractère.s que nous venons de lire.

-Les instructions de la dernière section permettent de lancer l’analyseur


lexicale, la première instruction yywrap() permet de regrouper les expressions
régulière ensemble. Ensuite, comme il s’agit d’un code qui sera transformé en
un code C et qui sera exécuté comme tel nous devons écrire la fonction pour
que le programme s’exécute. A l’intérieur du main l’instruction yylex() doit être
écrite pour permettre de lancer l’analyse lexicale sinon le programme
s’exécutera sans que rien ne se produise.

Une fois que l’analyseur lexical est écrit en lex, nous devons le compiler en
utilisant la commande flex.
Pour cela il faut ouvrir l’invité de commande là ou se trouve le fichier contenant
votre analyseur lexical et tapper :
flex nom_fichier.l
Cette commande va nous générer un fichier toujours nommé lex.yy.c, nous
compilerons ce fichier à l’aide de la commande gcc
gcc lex.yy.c
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 lexical, vous aller soit le lancer en tapant son nom
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
lexical. 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
gcc lex.yy.c
-o nom_ fichier.exe
fichier.exe ou bien nom_ fichier.exe<nom_fichier.txt

BON COURAGE

Vous aimerez peut-être aussi