Vous êtes sur la page 1sur 7

Compilation de langages de programmation C.

HEMDANI

L’outil Lex
Lex est un générateur automatique d’analyseurs lexicaux disponible dans tous les environ-
nements de développement en C sous U nix 1 . Il lit en entrée un fichier ”fichier.l” décrivant
l’analyseur lexical du langage à compiler et produit en sortie un fichier ”lex.yy.c” contenant le
code C de l’analyseur lexical décrit dans ”fichier.l”.

fichier.l Lex lex.yy.c


description d’un analyseur l’analyseur lexical
lexical: spécification Lex écrit en langage C

Le fichier ”fichier.l” contient principalement :


— les expressions régulières décrivant les tokens du langage,
— des fragments de code en langage C constituant les actions, et qui seront recopiés tels quels
dans le fichier de sortie ”lex.yy.c”.
Le programme ”lex.yy.c“ résultant contient principalement :
— une table implémentant l’automate construit à partir des expressions régulières,
— une fonction ”yylex()” qui réalise l’analyse en utilisant la table. Elle renvoie un entier
décrivant le token reconnu ; à la fin du texte source, elle retourne la valeur 0 2 .

1 Format d’une spécification Lex


Une spécification Lex (i.e., le fichier d’entrée ”fichier.l”) comporte trois parties séparées par
des lignes ne contenant que le symbole %% aligné à gauche :

Partie déclarations
%%
Partie règles
%%
Partie fonctions auxiliaires

1.1 La partie déclarations


Elle peut contenir :
1. du code C placé entre les séparateurs %{ et %} et recopié tel quel au début du fichier syn-
thétisé. Les marqueurs %{ et %} doivent être alignés à gauche et chacun sur une ligne. Ce
code peut contenir :
— des directives d’inclusion de fichiers (#include <...>),
— la déclaration d’un type énuméré décrivant les tokens,
— des déclarations de types énumérés décrivant des valeurs d’attributs,
— des variables globales utilisées dans les actions et les fonctions auxiliaires,
— ...
2. des définitions régulières de la forme :
d1 e1
d2 e2
.. ..
. .
dn en
où chaque notion di peut être utilisée dans une expression régulière ej (j > i) en la plaçant entre
{ et }.

1. On convient ici de regrouper sous le même terme ”Unix” toutes les variantes de ce système y compris Linux.
2. Ne pas utiliser cet entier pour coder un token autre que F IN .

1
Exemple 1
Le code suivant donne un exemple de partie déclarations.
%{
#include <stdio.h>

enum TOKEN {FIN=0, PV, IF, EGAL, ASSIGN, OP, PARG, PARD, ID, NUM};
%}

blancs [ \t\n]+
lettre [A-Za-z]
chiffre [0-9]
ident {lettre}({lettre}|{chiffre})*
entier {chiffre}+


1.2 La partie règles


C’est la partie principale de la spécification. On peut y trouver :
— des déclarations en langage C placées entre %{ et %} et qui seront placées au début du corps
de la fonction yylex() ; elles peuvent donc être utilisées dans les actions,
— des règles de la forme :

e1 action1
e2 action2
.. ..
. .
en actionn

où chaque (ei )i=1,n est une expression régulière et chaque (actioni )i=1,n est un fragment de
code C séparé de l’expression régulière par au moins un blanc ou une tabulation.

L’interaction entre l’analyseur lexical généré par Lex et un analyseur syntaxique (souvent généré
par yacc 3 ) se fait comme suit : quand il est activé (par l’analyseur syntaxique), l’analyseur lexical
lit le reste du texte source caractère par caractère jusqu’à reconnaître le plus long préfixe filtré
par un modèle ei ; il exécute alors l’action actioni qui, typiquement, rend le contrôle à l’analyseur
syntaxique via une instruction return token;. Si elle ne lui rend pas le contrôle, l’analyseur
lexical continue à reconnaître d’autres lexèmes jusqu’à ce qu’une action rende le contrôle ; ceci
permet par exemple, d’éliminer les blancs et les commentaires.

1.2.1 Les expressions régulières acceptées par Lex

Expressions simples

a : le caractère a.
”a” : le caractère a même si c’est un marqueur prédéfini.
\a : idem.
\nnn : le caractère de code octal nnn.
[abc] : a, b ou c.
[a-c] : idem.
[^a-c] : tout caractère n’appartenant pas à [a-c].
\n, \t, \r, \v ... : caractères prédéfinis du langage C.
. : tout caractère différent de \n.

Expressions composées

Soient e et f deux expressions régulières,

3. un générateur automatique d’analyseurs syntaxiques

2
(e) : e
^e : e au début d’une ligne.
e$ : e en fin de ligne.
e? : e est optionnel.
e* : 0 ou plusieurs fois e.
e+ : 1 ou plusieurs fois e.
e{n,m} : entre n et m occurrences consécutives de e.
e|f : e ou f.
e/f : e seulement s’il est suivi par f.
{d} : la notion d telle que définie dans la partie déclarations.

1.2.2 Les actions

Les actions sont typiquement de la forme :

return token;

Pour transmettre une valeur d’attribut, on positionne la variable globale yylval (lval pour last
value). Le dernier lexème reconnu est accessible par les variables :
— yytext : un tableau de caractères contenant le lexème,
— yyleng : une variable entière contenant la longueur du lexème.

Remarque 1

1. Si actioni est absente, l’action par défaut consiste à afficher le lexème reconnu.
2. Si une action comporte plus d’une instruction ou qu’elle ne peut pas tenir sur une ligne, elle
doit être placée entre { et }.
3. On peut utiliser le symbole | à la place d’une action pour indiquer que l’action est la même
que celle de la règle suivante.
4. Si une action est réduite à ’;’, le lexème reconnu est simplement ignoré. 2

1.3 La partie fonctions auxiliaires


Cette partie peut contenir des fonctions appelées dans les actions des règles ; elles seront reco-
piées telles quelles dans le fichier synthétisé. On peut y trouver :
— une fonction de gestion d’erreurs lexicales,
— la définition de la fonction yywrap() 4 ,
— une fonction principale main(),
— ...

2 Un exemple complet
Soit à implémenter un analyseur lexical pour le mini-langage décrit par la grammaire

P −→ P; P | I
I −→ if (E == E) I | id = E | ε
E −→ E op E | (E) | id | num

où op désigne l’un des opérateurs arithmétiques + ou − ; id est un identificateur constitué d’une


suite de lettres et chiffres commençant par une lettre ; num est un nombre entier constitué d’une
séquence non vide de chiffres décimaux.
L’analyseur lexical aura comme charge, en plus de retourner les tokens et leurs attributs,
d’ignorer les blancs, les tabulations, les caractères de fins de lignes et les commentaires. On sup-
posera que dans ce mini-langage, un commentaire commence par la marque // et se termine avec
la fin de la ligne.
Le tableau suivant donne une description des unités lexicales du mini-langage.

4. Voir la section ”Fonctions offertes par Lex”.

3
Token Modèle Attribut
F IN \0 | EOF
PV ;
IF if
EGAL ==
ASSIGN =
OP +|− COP ∈{P LU S, M OIN S}
P ARG (
P ARD )
ID lettre(lettre | chif f re)∗ nom
NUM chif f re+ valeur entière

Dans ce tableau, lettre et chif f re désignent, respectivement, l’expression régulière [A − Za − z]


et [0 − 9].
On donne dans ce qui suit la spécification Lex décrivant l’analyseur lexical qu’on s’est proposé
de réaliser.

%{
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

enum TOKEN {FIN=0, PV, IF, EGAL, ASSIGN,


OP, PARG, PARD, ID, NUM};

enum CODEOPERATION {PLUS, MOINS};

union { // Cette variable contiendra la valeur


char * nom; // d’attribut du dernier token reconu.
int valeur;
enum CODEOPERATION cop;
} yylval;

void erreur_lexicale();
%}
%option yylineno
blancs [ \t\n]+
lettre [A-Za-z]
chiffre [0-9]
ident {lettre}({lettre}|{chiffre})*
entier {chiffre}+
%%
{blancs} ;
; return PV;
if return IF;
== return EGAL;
= return ASSIGN;
[-+] { if (yytext[0]==’+’)
yylval.cop=PLUS;
else
yylval.cop=MOINS;
return OP;
}
\( return PARG;
\) return PARD;
{entier} { yylval.valeur=atoi(yytext);
return NUM;
}
{ident} { yylval.nom=(char*)malloc(yyleng+1);

4
strcpy(yylval.nom,yytext);
return ID;
}
"//".*$ ;
. erreur_lexicale();
%%
/* Avec la définition suivante de la fonction yywrap(), l’analyse s’arrête à
la fin de l’unique fichier accepté en argument.
* /
int yywrap(){
return 1;
}

/* La fonction erreur_lexicale() est appelée lorsqu’un caractère illégal est


rencontré. Elle indique le numéro de ligne et affiche le caractère illégal
s’il est imprimable.
*/
void erreur_lexicale(){
printf("ligne %d : ",yylineno);
if (isprint(yytext[0]))
printf("’%c’ ",yytext[0]);
printf("caractère illégal.");
exit(1);
}

/* En pratique, la fonction principale main() n’est pas nécessaire; sa


présence ne se justifie que par l’absence d’un analyseur syntaxique
qui ferait appel à la fonction yylex().
*/
int main(int argc, char * argv[]){
enum TOKEN tc;
if (--argc>1){
printf("Usage:\n\t %s [nom_de_fichier]\n",argv[0]);
exit(2);
}
else if (argc==1){
if ((yyin=fopen(argv[1],"r"))==NULL){
printf("Impossible d’ouvrir le fichier <%s>.\n",argv[1]);
exit(3);
}
}
else
printf("Taper un programme:\n\n");
while (tc=yylex()){
switch(tc){
case PV: printf("<PV, >\n"); break;
case IF: printf("<IF, >\n"); break;
case EGAL: printf("<EGAL, >\n"); break;
case ASSIGN: printf("<ASSIGN, >\n"); break;
case OP: printf("<OP, ");
if (yylval.cop==PLUS)
printf("PLUS>\n");
else
printf("MOINS>\n");
break;
case PARG: printf("<PARG, >\n"); break;
case PARD: printf("<PARD, >\n"); break;
case ID: printf("<ID, \"%s\">\n",yylval.nom); break;
case NUM: printf("<NUM, %d>\n",yylval.valeur);
}
}

5
printf("<FIN, >\n");
return 0;
}
L’option yylineno apparaissant juste avant la définition régulière, signifie à Lex qu’il doit main-
tenir la valeur d’une variable de même nom indiquant le numéro de la ligne en cours d’analyse.
Après génération et compilation du code source de l’analyseur lexical 5 , l’analyse du programme
de test suivant :

// prog.ml : Exemple de programme du mini-langage


// -------------------------------------------------------------

max=100; // Initialisation
if (x1==max) // Test pour l’égalité
y=ifs+0013-max

produit la séquence de couples <token, attribut> suivante :


<ID, "max">
<ASSIGN, >
<NUM, 100>
<PV, >
<IF, >
<PARG, >
<ID, "x1">
<EGAL, >
<ID, "max">
<PARD, >
<ID, "y">
<ASSIGN, >
<ID, "ifs">
<OP, PLUS>
<NUM, 13>
<OP, MOINS>
<ID, "max">
<FIN, >
2

3 Actions prédéfinies de Lex


Lex offre plusieurs actions (des macro-instructions) dont les plus utilisées sont :

ECHO; : affiche le contenu de yytext.

REJECT; : quand REJECT; est placée à la fin d’une action, elle force l’analyseur à soumettre
la même entrée aux règles suivantes.

Exemple 2
Pour calculer les nombres d’occurrences des mots ”compiler” et ”pile” dans un texte, on peut
utiliser les règles suivantes :
compiler { nombre_compiler++; REJECT; }
pile { nombre_pile++; }
\n |
. ;

Ces règles se justifient par le fait que le mot ”pile” apparaît dans le mot ”compiler”. 2

5. Voir la section ”Commandes de compilation”.

6
4 Fonctions offertes par Lex
Plusieurs fonctions sont utilisées par l’analyseur lexical généré par Lex et sont disponibles pour
l’utilisateur. Les plus importantes sont :

int yylex() : c’est la fonction qui réalise l’analyse lexicale ; à chaque appel, elle
retourne un nombre entier positif indiquant un token, ou 0 à la fin
du fichier.

int yymore() : indique que le lexème reconnu lors du prochain appel à yylex()
sera concaténé au contenu de yytext au lieu de le remplacer.

int yyless(int n) : indique que seuls les n premiers caractères de yytext seront gar-
dés ; ceux qui restent (s’il y en a) seront considérés comme non
encore lus lors du prochain appel à yylex().

int input() : retourne le caractère suivant du flux d’entrée ; elle retourne 0 à la


fin du fichier.

int unput(int c) : replace le caractère c dans le flux d’entrée.

int yywrap() : elle est appelée par yylex() à la fin du fichier d’entrée. Si la va-
leur retournée par yywrap() est 1, l’analyse s’arrête. Pour pou-
voir analyser plusieurs fichiers, il faut définir la fonction yywrap()
pour permettre :
− d’ouvrir le fichier suivant et l’associer à la variable FILE * yyin ;
− de retourner la valeur 0.

Remarque 2
Ce n’est que lorsque yywrap() retourne 1 que yylex() retourne 0. 2

5 Commandes de compilation
Supposons qu’une spécification Lex soit contenue dans un fichier de nom ”analex.l” ; la com-
mande

$ lex analex.l
produit un fichier de nom ”lex.yy.c” contenant le programme source en C de l’analyseur lexical
décrit par la spécification.
Pour compiler le programme, il suffit de saisir la commande
$ gcc lex.yy.c
qui produira un fichier exécutable de nom ”a.out”.
Au lieu d’utiliser les noms par défaut, on peut nommer les fichiers générés en utilisant l’option
”-o” (de output) ; ainsi, la commande
$ lex -o analex.c analex.l
ordonne à Lex de placer le programme source de l’analyseur lexical qu’il produit, dans un fichier
de nom ”analex.c”, et la commande
$ gcc -o analex analex.c
donne le nom ”analex” au programme exécutable produit par la chaîne de développement gcc.

Vous aimerez peut-être aussi