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 :
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.
Définitions (partie facultative)
Cette partie contient les définitions opératoires
(lignes commençant par %)
et les définitions d'expressions.
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.
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.
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 (%snomconfig) qui seront
utilisées dans la troisième partie (voir BEGINcondition),
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.
BEGINconfiguration
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.
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() :
(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.