Organisation du fichier source pour l'utilitaire LEX

Un fichier en quatre parties

Un fichier-lex (fichier source pris en entrée par Lex) contient quatre parties successives :
  1. Déclarations (partie facultative) : code C(1) préalable (inclusions, déclarations, etc.).
    Cette partie commence par  %{  et finit par  %}  chacun de ces délimiteurs devant être écrit sur une ligne ne contenant rien d'autre(2). Toutes les lignes de code C écrites entre ces deux délimiteurs seront directement recopié par Lex au début du fichier source lex.yy.c.
  2. Définitions (partie facultative)
    Cette partie contient les définitions opératoires (lignes commençant par %) et les définitions d'expressions.
  3. Règles (=productions)
    Cette partie, qui est la seule obligatoire, commence par le délimitateur  %%  écrit seul dans sa ligne(2). Elle contient une suite (éventuellement vide) de règles (une ligne par règle) qui sont des expressions rationnelles augmentées : l'expression rationnelle est suivie, après un (ou plusieurs) espace(s) et/ou tabulation(s), du code C(1) qui lui est associé et qui sera exé chaque fois qu'un motif correspondant à cette expression est reconnu.
  4. Code utilisateur.(partie facultative*)
    Cette partie commence par le délimiteur  %%  seul dans sa ligne(2). Toutes les lignes de code C(1) écrites après seront directement recopiées par Lex dans le fichier lex.yy.c. Cette partie permet notamment de redéfinir les fonctions yywrap() et main(), si on n'utilise pas ces fonctions prédéfinies dans la librairie de Lex (option −ll à la compilation avec Lex, −lfl avec Flex).
    * Attention : pour Flex, le délimiteur initial %% de cette partie est obligatoire.
(1) Dans les parties où il faut ajouter du code C, on peut placer du code C++, lex.yy.c sera alors compilé avec un compilateur C++. Mais le code produit automatiquement par Lex restera du code C, ce qui peut produire quelques difficultés supplémentaires. Ainsi, l'ajout de code C++ nécessite parfois de définir dans la partie déclaration certaines fonctions définies trop tard dans le code  créé par Lex ; selon les erreurs indiquées à la compilation, on pourra être amené à ajouter, par exemple, les prédéclarations : yylook();  et yyback(int * p, int m); dans la première partie du fichier-lex.
(2) Pour être plus précis, ce délimiteur doit seulement être placé en début de ligne (sans indentation). Je conseille ces précautions supplémentaires car la présence d'espace ou de tabulations "mal" placées, voire de caractères masqués (gare aux copier-coller et aux FTP!) dans le fichier-lex peut perturber le travail de Lex. Ce manque de robustesse et de convivialité est un des défauts de Lex.

Quatre exemples de fichiers-lex

Exemple 1
%%

Exemple 2
%%
[*+/-]   printf("opérateur\n");
[0-9]+   printf("nombre\n");
.|\n ECHO;

Exemple 3
%s condition1
%s condition2
%%
<INITIAL,condition1>^C$      {BEGIN (condition2);}
<INITIAL,condition2>^V$      {BEGIN (condition1);}
<condition1,condition2>^I$   {BEGIN(INITIAL);}
<condition1>[aeiou]          {printf("-");}
<condition2>[b-df-hj-np-tv-z]   {printf("+");}

Exemple 4
%{
int v=0;
int c=0;
%}
voyelle [aeiou]
consonne [b-df-hj-np-tv-z]
%%
{voyelle}    {printf("-"); v++;}
{consonne}   {printf("+"); c++;}
.       {return 1;}
\n      {printf("\nc=%d, v=%d\n",c,v); return 0;}
%%
main() {
 while (yylex()==0);
}

Précisions sur les parties de Lex

1e partie : Le code préalable

Il sert essentiellement à insérer des directives de précompilation C (#ifdef, ...), à inclure des bibliothèques et des sources extérieurs (#include, ...), et à déclarer des variables et constantes globales. On peut aussi y définir les fonctions et procédures qui serviront dans le code associé aux expressions (troisième partie) et/ou dans le code utilisateur (quatrième partie).

2e partie : Les définitions

Les définitions opératoires contiennent notamment les déclarations de configurations (%s nomconfig) qui seront utilisées dans la troisième partie (voir BEGIN condition), en plus de la condition INITIAL prédéfinie par Lex comme condition par défaut. On peut ainsi imposer à l'analyseur différents comportements selon la configuration dans laquelle il est placé. La configuration de l'analyseur en début de traitement est prédéfinie sous le nom INITIAL ( généralement codé par la valeur 0). On peut ainsi réaliser une analyse multilangage (par exemple l'analyse d'un code HTML doit distinguer l'analyseur des balises, début avec un caractère '<' et fin avec un caractère '>', de celui du corps du texte.

L'exemple 3 définit deux configurations condition1 et condition2 par, respectivement,  %s condition1  et  %s condition2

Les définitions d'expressions sont de la forme :  NOM  EXPRESSION  où NOM est un mot composés de lettres majuscules et minuscules (plus '_' autorisé sauf au début du mot) et où EXPRESSION est une expression régulière écrite selon les conventions de Lex (voir lexer.htm).

L'exemple 4 définit une expression voyelle par  voyelle [aeiou],  et une expression consonne par   consonne [b-df-hj-np-tv-z]

3e partie : Les régles

Les expressions rationnelles sont définies selon des conventions particulières à Lex et inspirées de l'environnement C/Unix (voir lexer.htm) ; les motifs reconnus par Lex dépassent en fait le cadre formel des expressions rationnelles. Pour chaque motif, on peut préciser les configurations dans lesquelles ils sont actifs sous la forme :  <ListeDeConfigurations>MOTIF. L'analyse lexicale peut ainsi prendre une dimension contextuelle.

L'exemple 3 modifie les voyelles, avec printf("-") comme code associé à l'expression reconnaissant une voyelle, ou bien modifie les consonnes, avec printf("+"), ou laisse le texte intact. L'analyseur donne à l'utilsateur le choix de la configuration à appliquer : bascule dans la configuration condition1 par la commande V (caractère seul sur une ligne), dans la configuration condition2 par la commande C, retour à la configuration INITIAL par la commande I. Selon la configuration, le texte ressort inchangé, ou avec les voyelles remplacées par un tiret, ou avec les consonnes remplacées par un plus.

Le code C associé aux expressions utilise des variables prédéfinies par Lex :
yytext   est le tableau de caractères où est placé le motif extrait du flot d'entrée. yytext[0] est son premier caractère et yytext[yyleng−1] son dernier.
N.B. yytext n'est donc pas défini comme un char* ...
yyleng   est la taille de yytext (son nombre de caractères).
yylval   est la variable de type YYTYPE permettant la communication d'une valeur associée au motif reconnu de Lex vers Yacc.
Par défaut YYTYPE est de type int, il peut être redéfini.
yyin définit(3) le flux entrant dans lequel sont reconnus les motifs successifs. Par défaut yyin est stdin, la saisie au clavier (redirigeable avec l'opérateur Unix <).
yyout définit(3) le flux sortant (qui est utilisé par fonction ECHO). Par défaut yyout est stdout, la sortie texte à l'écran (redirigeable avec l'opérateur Unix >).
...   etc.
Ce code peut contenir des fonctions et procédures prédéfinies par Lex :
ECHO   affiche la chaine de caractère correspondant à yytext et qui correspond au motif reconnu et extrait du flot d'entrée.
BEGIN configuration   place l'analyseur dans la configuration indiquée.
REJECT   replace le motif reconnu dans le flot d'entrée.
yymore()   conserve le motif courant dans yytext ; le motif reconnu suivant sera ajouté en suffixe de yytext, au lieu d'écraser cette précédente valeur.
yyless(n)   fonctionne comme yymore() mais en supprimant au préalable les n premiers caractères du motif courant.
... etc.

Absence de règle :
Par défaut, tous les caractères du flot d'entrée ne pouvant être intégrés à un motif reconnu par une règle est redirigé sans changement vers le flot de sortie (comme si Lex/Flex ajoutait une règle finale :  . {ECHO.}  ou bien  .|\n {ECHO.}).

Gestion des conflits de règles :
La résolution de conflits de règles peut être différente selon qu'on a affaire à Lex (sous Unix) ou Flex (sous Linux).
– Règles équivalentes :
%%
aaa  
a{3}  
%%
 
{premiere++;}
{deuxieme++;}
 
Ces deux règles reconnaissent le même motif a3.
Lex, comme Flex, n'utilisera dans ce cas que la première règle. La seconde règle sera reconnue comme redondante et donc ignorée. Dans cet exemple, seule la variable premiere sera incrémentée.
– Règles subsumées (motifs inclus) :
%%
a  
aa  
aaa  
%%
 
{simple++;}
{double++;}
{triple++;}
 
 
%%
aaa  
aa  
a  
%%
 
{triple++;}
{double++;}
{simple++;}
 
On a : "a" ⊆ "aa" ⊆ "aaa".
Lex choisit dans ce cas l'expression qui peut sélectionner le plus long motif ("aaa" plutôt que "aa" ou "a", "tout" plutôt que "to", etc.). Ces deux exemple donneront donc des résutats différents si on saisit "aaaaaaaaaaa" (onze fois 'a') : triple=0, double=0, simple=11, dans le premier exemple, et triple=3, double=1, simple=0, dans le deuxième.
Flex choisit au contraire la première règle utilisable. Ces deux exemples donneront le même résultat si on saisit "aaaaaaaaaa" (onze fois 'a') : triple=3, double=1, simple=0.
N.B. Pour l'exemple suivant utilisant la fonction REJECT, Lex, comme Flex, donnera, si on saisit "aaaaaaaaaa" (onze fois 'a') : triple=9, double=10, simple=11.
%%
aaa  
aa  
a  
%%
 
{triple++; REJECT;}
{double++; REJECT;}
{simple++;}
 

4e partie : Le code utilisateur

La compilation de lex.yy.c avec l'option −ll (−lfl avec Flex) ajoute avant compilation les définitions des fonctions yywrap(4) et main définies par défaut par :
  int yywrap() {return 1;}
  main() {return yylex();}
Quand une de ces fonctions est définie directement par l'utilisateur dans la quatrième partie du fichier-lex, la définition faite par l'utilisateur est prioritaire, pourvu que l'option &minusll/−lfl soit placée à la fin de la commande de compilation. La fonction main() devra alors contenir un appel à yylex()(5), fonction principale de l'analyseur lexical.
 
(3) Par défaut, le flux d'entrée est le flux stdin. On peut, par exemple, utiliser en entrée un fichier dont le nom est passé en argument de l'exécutable en reprogrammant main() :
main(int argc, char** argv) {
  if (argc>1) {
    FILE* fichier;
    fichier=fopen(argv[1],"r");
    if (fichier==NULL) { fprintf(stderr,"fichier %s introuvable\n",argv[1]); return EXIT_FAILURE; }
  yyin=fichier;
  yylex();
  return EXIT_SUCCESS;
}
(4) La fonction yywrap() détermine la conduite à tenir lorsque le flot d'entrée est épuisé : arrêter l'analyse (return 1; valeur par défaut) ou continuer avec un autre flot (return 0;). Ce dernier cas est plus particulièrement utilisé quand le flot d'entrée est un fichier et qu'on se donne la possibilité de poursuivre l'analyse sur un autre fichier (la gestion des ouvertures de fichiers devra être prévue dans le code). Quand la fonction yywrap est inutile, on peut ajouter une ligne  %option noyywrap dans la deuxième partie du fichier-lex.
(5) La fonction yylex() est appelée récursivement tant qu'il reste du texte à analyser. Chaque appel utilise une règle du fichier-lex et place dans yytext le texte reconnu par la règle. En fin de traitement, yylex() appelle yywrap() et s'il n'y a pas de nouveau flot d'entrée termine en retournant 0. L'instruction  return n;  placée dans le code associé à une règle se retrouvera dans le code de yylex() et mettra donc fin à cette fonction en retournant la valeur n. Cette valeur peut être utilisée par la fonction main(), notamment quand Lex est interfacé avec Yacc (voir ce dernier).

Paramètre de compilation de lex.yy.c

 −ll   doit être placée à la fin de la ligne de compilation, le compilateur peut alors vérifier si les fonctions yywrap() et main() sont définies avant de les rajouter si nécessaire.

Paramètres de Lex

 −t  
 −v  
 −h  
 −f  
 
...
affiche le code source produit sans créér le fichier lex.yy.c
affiche des informations sur l'automate créé par Lex.
affiche l'aide de Lex
pour une compilation plus rapide au détriment de la compacité du code.
etc.