Jul 03, 2024

Wiki

Python

Aide

edit SideBar

Search

Lex


Le but de ce TP est de mieux comprendre l'analyse lexical, grâce à la découverte d'un outil dévolu à cette tâche : lex.

Introduction

Lex, qu’est-ce que c’est ?

Lex est un préprocesseur qui, à partir d’un fichier source (respectant la syntaxe lex), produit un fichier C.

C’est une aide à la programmation : le fichier lex que vous avez saisi est souvent bien plus court que le fichier C que vous auriez dû taper.

Vous pourrez utiliser lex à chaque fois que vous aurez à concevoir un analyseur lexical en C.

On rappelle qu’un analyseur lexical est un programme qui reçoit en entrée un flot de caractères (en provenance de l’entrée standard, ou d’un fichier) et qui le découpe en unités lexicales de base : les lexèmes (token).

Installation

Normalement, quelle que soit votre distribution GNU/Linux, vous devriez déjà avoir lex.

Si cependant, dans un terminal, la commande lex ne répond pas, vous devrez peut-être installer le package flex.

Premiers éléments sur lex

L'exemple de base

Voici notre premier programme lex. On le tapera dans un éditeur de textes, et on le sauvegardera sous le nom exemple0.l (l'extension réglementaire pour un fichier lex est .l) :

  %{
  %}
  %%
  %%

Voici la marche à suivre pour le compiler et l'exécuter. Dans un terminal, à l’endroit où se trouve exemple0.l, tapez :

  $ lex exemple0.l

On obtient alors un fichier C au nom de lex.yy.c, qui contient plusieurs milliers de lignes, chargées en commentaires (vous pouvez l’éditer pour vous en assurer)...et qui ne fait rien !

On va le vérifier. La commande...

  $ cc -o nomExecutable lex.yy.c -ll

compile le fichier lex.yy.c (vous pouvez remplacer nomExecutable par le nom de votre choix ; quant à l’option -ll, elle sert à inclure la bibliothèque lex /usr/lib/libl.a).

On exécute alors :

  $ ./nomExecutable

Une nouvelle ligne apparaît. Saisissez du texte, puis validez : ledit texte est recopié dans le terminal, sans modification. Bref, vous lui avez dit de ne rien faire (ne pas toucher au flux d’entrée), donc il ne fait rien.

Pour quitter, Ctrl-D.

Eléments de structure d'un fichier lex

Les symboles %{, %}, et %% (à deux endroits) sont obligatoires, dans l’ordre d’exemple0.l, et en début de ligne. Lex est rigide à bien des égards, vous vous en rendrez bien vite compte.

Ces symboles séparent le fichier lex en quatre parties, pouvant chacune être vide...

Les quatre parties d’un fichier Lex sont :

  1. Une en-tête, entre %{ et %}, dans laquelle on peut placer le code C que l’on souhaite avoir au début du fichier C généré,
  2. Une définition de symboles non terminaux et de contextes, entre %} et %%,
  3. Une partie réservée aux règles définissant les lexèmes reconnus, entre le premier %% et le dernier,
  4. Enfin, une partie principale, suivant le dernier %%, qui sera recopiée intégralement dans le fichier C généré, et qui est la place idéale pour définir ses fonctions et son main().

Un mot sur les commentaires

On peut faire des commentaires de la même manière que pour un fichier C, mais pas n’importe où.

Ainsi, un commentaire en première ligne d’un fichier lex engendrera une erreur ; de même, une ligne de règle ne peut pas commencer par un //.

Prenez l’habitude d’écrire un commentaire dans la partie 1 (entre %{ et %}) pour préciser ce qu’est sensé faire le programme.

Exemple approfondis

On souhaite, dans ce qui suit, filtrer les chiffres.

On récupère, à partir de notre flux entrant (le terminal, un fichier...), des caractères, et si ces caractères ne sont pas des chiffres, on les recopie vers la sortie standard.

On souhaite donc que si l’analyseur rencontre un chiffre, il le filtre : c’est une règle (donc partie 3), associée au motif correspondant à un chiffre.

Le ou se traduit en lex par des crochets : [0123456789] signifie donc un chiffre. C’est un peu long à saisir, aussi on peut le noter [0-9].

Dans le même esprit, les lettres b,c ou d se note [bcd], quand une lettre minuscule s’écrit [a-z].

Que faire s’il rencontre le motif [0-9] ? rien. D'où le fichier filtreChiffre.l :

  %{
  // Filtre les chiffres
  %}
  %%
  [0-9]
  %%

Tous les motifs situés entre %% et %% sont interceptés, les autres sont recopiés sans changement à la sortie standard. C’est exactement ce que l’on souhaitait.

Supposons que, après lex et cc, l’exécutable s’appelle filtrerChiffre, et que l’on ait un fichier texte à filtrer, portant le nom de toto.

Cela peut se faire tout simplement :

  $ ./filtreChiffre < toto

On peut aussi rediriger la sortie vers un fichier toto2 :

  ./filtreChiffre < toto > toto2

Améliorons le programme précédent. Supposons que l’on souhaite compter les chiffres d’un fichier.

En face du motif [0-9], on peut associer l’incrémentation d'un compteur :

  [0-9] compteChiffre++ ;

Que l’on aura soin d’initialiser dans la partie 1...

  %{
  // Compte les chiffres
  #include <iostream>
  using namespace std ;
  int compteChiffre=0 ;
  %}

...et d’afficher dans le main() de la partie 4, après avoir lancé l’analyseur lexical par un appel à la fonction yylex() :

  int main(){
  yylex() ;
  cout << compteChiffre <<endl ;  
  }

Cela donne au final ça...

  %{
  // Compte les chiffres
  #include <iostream>
  using namespace std ;
  int compteChiffre=0 ;
  %}
  %%
  [0-9] {compteChiffre++ ;}
  %%
  int main(){
      yylex() ;
      cout << compteChiffre <<endl ;
  }

...que l’on compile comme d’habitude :

  $ lex compteChiffre.l
  $ g++ -o compteChiffre lex.yy.c -ll
  $ ./compteChiffre < toto

où toto est le fichier dont on souhaite compter les chiffres.

Travaux pratiques

  1. Filtrez les lettres (majuscules et minuscules) d’un fichier,
  2. Comptez les lettres (majuscules d’une part, minuscules d’autre part) d’un fichier.

Structure détaillée d’un fichier Lex

Un fichier Lex se compose de quatre parties distinctes :

  1. l’en-tête,
  2. la définition de symboles non-terminaux et de contextes,
  3. les règles,
  4. la partie principale.
  %{
en-tête
  %}
  définition de symboles non-terminaux et de contextes
  %%
  règles
  %%
  partie principale

Comment faire une en-tête lex ?

  • Cette partie commence par %{, et se termine par %}.
  • Ces symboles doivent être consécutifs, en début de ligne, et seuls sur la ligne !
  • Elle est obligatoire, mais peut n’avoir aucun contenu.

Pourquoi faire une telle en-tête ?

  • Pour y mettre du code C, qui viendra se placer en tête du fichier résultat.
  • On peut, en particulier, y placer :
    • des directives comme include, ou define,
    • des déclarations de variables globales,
    • le code de fonctions que l’on désire utiliser dans la suite.
  • Enfin, c’est l’endroit idéal pour placer un commentaire précisant le but du fichier.

Vient alors la définition de symboles non-terminaux et de contextes. Pourquoi définir de tels symboles ? D’abord...

  • pour donner un nom à un motif, comme par exemple : ENTIER [0-9]+ (C’est-à-dire : ENTIER représentera, dans la suite, une succession d’au moins un chiffre.)
  • afin de rendre les définitions de motifs de la partie règle plus parlant, pour gagner en clarté.

Ensuite, pour les contextes :

  • ils sont introduits par %s,
  • la définition d’un contexte permet de faire varier l’interprétation de certains symboles suivant l’environnement.
  • Après l’en-tête, entre les symboles %} et .
  • Cette partie peut être vide. Il ne peut y avoir qu’une définition par ligne (le nom, suivi du motif.)

Des règles, pour quoi faire ?

Pour définir les lexèmes reconnus par le langage qui va être analysé.

Une règle se compose de deux parties :

  1. la définition du motif,
  2. l’action associée.

Action associée à une règle

L’action est une suite d’instructions en langage C, indiquant ce qu’il faut faire à chaque fois que le motif est reconnu.

Par exemple :

  [0-9] compteChiffre++ ;

signifie que s’il l’on rencontre un chiffre (le motif), on incrémente le compteur associé d’une unité.

Comment faire une règle ?

Elles commencent par les caractères (accolés, en début de ligne, etc.) et se terminent de même.

Le motif utilise la syntaxe des expressions rationnelles sed ou egrep.

Entre le motif et l’action associée, il faut au moins un espace ou une tabulation, le tout tenant sur une même ligne.

S’il y a plus d’une action pour un motif donné, on doit encadrer ces actions par des accolades.

La recherche du motif s’arrête naturellement à la fin du fichier.

Dans le cas où...

  • l’on souhaite uniquement ignorer un motif, il faut quand même mettre un point-virgule pour l’action associée : [0-9] ;
  • l’on a, pour plusieurs motifs, la même action à faire : on place le caractère | (tube) sur toutes les lignes, sauf la dernière (qui contient ladite action), comme par exemple
   [a-z] |
   [A-Z] printf("Une lettre\n") ;
  • l’on souhaite récupérer le motif : il se trouve dans yytext. Cette variable, déclarée par lex, est un pointeur sur la chaîne de caractères contenant le motif reconnu. Par exemple :
  [0-9]+ printf("Nombre entier reconnu : %d \n",atoi(yytext)) ;
  • l’on souhaite arrêter de rechercher des motifs (avant la fin du fichier) : on place, dans l’action considérée, un return qqch, ou qqch est de type entier ou énuméré.

Choix des motifs

Il se peut qu’une expression puisse marcher avec plusieurs motifs.

Par exemple, si l’on a

   .           {printf("un symbole") ;}
   [a-zA-Z]+   {printf("un mot" ;)}
...alors l’expression toto peut convenir pour chacune des deux lignes.
  • Dans ce cas, on choisit la deuxième ligne, puisque le lexème formé est le plus grand (t pour la première ligne, toto pour la deuxième.)
  • En cas d’égalité de tailles de lexèmes, on choisit « la première ligne qui marche ». Ainsi, si la chaîne de caractères analysée était simplement t, alors le code précédent afficherait : un symbole.

La partie principale

Passons maintenant à la partie principale :

  • Cette partie est recopiée intégralement dans le fichier C résultat.
  • On peut y placer la définition des fonctions utilisables dans la suite, ainsi que la fonction main().

Cas de l’absence d’un main()

S’il n’y a pas de main(), lex écrira :

  int main(){
      yylex() ;
  }

yylex(), fonction sans paramètre et retournant un entier, définie par le lex (vous n’avez pas à vous en préoccuper), est la fonction d’analyse lexicale écrite par le lex à partir de vos règles.

Vous devrez l’appeler, dans le cas où vous faites votre propre main(), si du moins vous souhaitez que votre flux soit analysé.

Les expressions rationnelles

On rappelle ci-dessous les règles des expressions rationnelles, adaptées à lex.

x
Correspond au caractère ’x’.
.
N’importe quel caractère (octet) sauf le retour à la ligne.
[xyz]
Une « classe de caractères » ; dans ce cas, le motif convient pour un ’x’, un ’y’, ou un ’z’.
[abj-oZ ]
Une « classe de caractères » contenant un intervalle ; ici, convient pour un ’a’, un ’b’, n’importe quelle lettre allant de ’j’ à ’o’, ou un ’Z’.
[^A-Z]
Une « classe de caractères niée », c.-à-d. tout caractère sauf ceux dans la classe. Dans cet exemple, tout caractère SAUF une lettre majuscule.
[^A-Z\n]
Tout caractère SAUF une lettre majuscule ou un retour à la ligne.
r*
Zéro ou plusieurs r, où r est une expression rationnelle quelconque.
r+
Un ou plusieurs r.
r?
Zéro ou un r.
r{2,5}
Entre 2 et 5 r.
r{2,}
Deux r ou plus.
r{4}
Exactement 4 r.
"[xyz]\"foo"
La chaîne de caractères littérale : [xyz]"foo.
\X Si
X est ’a’, ’b’, ’f’, ’n’, ’r’, ’t’ ou ’v’, alors la représentation C ANSI de \x. Sinon, un ’X’ littéral (utilisé pour protéger des opérateurs comme ’*’)
\0
Un caractère NUL (de code ASCII 0)
\123
Le caractère de valeur octale 123.
\x2a
Le caractère de valeur hexadécimale 2a.
(r)
Reconnaît un r ; les parenthèses sont utilisées pour surcharger la priorité.
rs
L’expression rationnelle r suivie de l’expression rationnelle s ; appelé « concaténation ».
r|s
Un r ou un s.
r\s
Un r mais seulement s’il est suivi par un s.
^r
Un r, mais uniquement au début d’une ligne (c.-à-d. au début de l’analyse, ou juste après qu’un saut de ligne ait été détecté).
r\$
Un r, mais seulement à la fin d’une ligne (c.-à-d. juste avant un saut de ligne). Équivalent à « r/\n ».
<s>r
Un r, mais seulement dans la condition de démarrage s. Voir ci-dessous l'exercice sur la suppression de commentaires.
<s1,s2,s3>r
Idem, mais dans une des conditions de démarrage s1, s2 ou s3. Voir ci-dessous.
<*>r
Un r dans n’importe quelle condition de démarrage, même une exclusive. Voir ci-dessous.
<<EOF>>
Un end-of-file (fin de fichier).
<s1,s2> <<EOF>>
Un end-of-file quand on se trouve dans la condition de démarrage s1 ou s2. Voir ci-dessous.

Travaux pratiques

Énoncés

  1. Faire un programme qui compte le nombre de lettres, de chiffres et d’autres symboles dans un texte.
  2. Remplacez toutes les majuscules d’un texte par des minuscules.
  3. Écrire un fichier lex qui reconnaît différents mots-clé d’un langage de programmation (le langage C, par exemple.)
  4. Écrire un fichier lex qui se comporte comme la commande système wc. Comparer vos résultats avec ceux de la commande.
  5. Écrire un fichier lex qui supprime les commentaires d’un programme écrit dans le langage de programmation de votre choix.

Page Actions

Recent Changes

Group & Page

Back Links