Lexers 1/2 : Théorie
Date de publication : 21/11/04
Cette première partie de l'article "lexers" a pour but de vous présenter dans un aspect théorique l'outil d'analyse
qu'est le lexer.
Ce document a pour suite disponible prochainement.
Mes autres articles se trouvent sur
Introduction
I. Qu'est-ce qu'un lexer ?
I-1. Définition
I-2. Quelques exemples
II. Comment ça marche ?
II-1. Introduction
II-2. Un peu de vocabulaire
II-3. Fonctionnement et utilisation
II-3.a. Analyse descendante
II-3.b. Côté lexer
II-3.c. Côté application
III. Quand utiliser un lexer ?
Conclusion
Nota
Références
Remerciements
Introduction
"lexer"... Vous avez peut-être déjà lu ou entendu ce mot... C'est bien gentil, mais qu'est-ce que
c'est ? Et à quoi ça sert, comment ça marche ?!
Dans ce tutoriel, c'est ce que je compte vous expliquer, en commençant par cette première partie théorique
suivie d'une partie plus pratique, de codage principalement !
La partie théorique que voici n'a pas la prétention de présenter dans son intégralité tout ce qu'on peut
trouver sur les lexers. L'ambition est de vous introduire au sujet, de vous y donner goût, en présentant
principalement l'utilité d'un lexer et son mode de fonctionnement global.
Ceci fait, nous enchainerons en toute logique sur la seconde partie, pratique, pour mettre en oeuvre
la méthode apprise et ainsi aboutir à un lexer fonctionnel !
I. Qu'est-ce qu'un lexer ?
I-1. Définition
Un lexer est un outil d'analyse, et particulièrement d'analyse de texte, sous toutes les
formes que cela puisse être : simple document, document formaté, script, code, ligne de calculs...
Un lexer va fournir au développeur un moyen puissant pour parcourir, décomposer et analyser un bloc de texte
structuré.
Il existe plusieurs types de lexers, qui ne fonctionnent pas tous de la même manière... Plus particulièrement,
il faut en réalité développer un lexer par structure que l'on veut analyser. Le pourquoi du comment de
cette spécificité est expliqué en deuxième partie.
I-2. Quelques exemples
Voici une liste de quelques exemples d'applications utilisant un/des lexer(s), et à quelles fins elles
les utilisent :
- Delphi : Coloration syntaxique, compilation du code, établissement de la liste des classes d'une unité...
- PHP : Recherche de PHP dans du HTML, exécution du code PHP...
- MySQL : Exécution des requêtes SQL
- Maple, MatLab : Interprétation/exécution de lignes de calculs mathématiques
- Word : Correction orthographique, formatage de texte (rtf, doc)...
- Excel : Interprétation/exécution de formules
- Mozilla : Lecture de pages HTML, exécution de javascript dans une page...
Toutes ces applications utilisent, d'une manière ou d'une autre, un outil d'analyse que l'on peut appeler
lexer, du plus simple (recherche de mots pour la correction orthographique) au plus complexe (compilation de code) !
II. Comment ça marche ?
II-1. Introduction
Dans ce paragraphe, il n'est pas question de fournir d'ores et déjà du code correspondant au
processus de traitement d'un lexer. Il s'agit de donner les bases de fonctionnement d'un lexer
en général.
Qui plus est, ce n'est pas l'unique solution pour développer un lexer, mais la
méthode est calquée sur un certain nombres de lexers, dont celui implémenté dans la classe TParser
de Delphi (voir l'unité classes.pas). Cette méthode est d'après moi, une approche assez intéressante
et qui, surtout, s'applique dans un bon nombre de cas d'utilisation !
La méthode en question est une méthode "séquentielle" : le flux de caractères fourni est parcouru
du début jusqu'à la fin et ce de maniére unidirectionnelle, c'est-à-dire sans possibilité de faire marche arrière
Voyons maintenant plus en détails ses caractéristiques...
II-2. Un peu de vocabulaire
Il n'y a pas foule de vocabulaire à définir... En réalité, il est particulièrement un mot qui mérite
d'être explicité car il sera souvent utilisé : token.
Je ne m'attacherai pas à essayer d'en donner une traduction acceptable en Français, vu que nous n'utiliserons
de toute façon que le terme anglais.
Un token est en quelque sorte le plus petit élément "unitaire" des données que nous voulons analyser.
Comme il s'agit dans notre cas de texte, nous appellerons token tout mot, groupe de mots ou de caractères
(lettres, chiffres, point...) constituant un tout, relativement à la structure du texte analysé.
Je m'explique par un exemple : prenons le cas d'un code en Pascal Objet, et ici une simple fonction :
| Du code delphi sans autre intérêt que de montrer quelques tokens... :o) |
procedure AfficherMessage(IdMsg: Integer; Texte: String);
var Msg: String;
Sng: Single;
begin
Sng := 1.5;
Msg := Format('Message ID: %f'#10#13'%s', [IdMsg * Sng, Texte]);
ShowMessage(Msg);
end; |
J'imagine que la coloration syntaxique de code de ce genre vous est relativement familière, et cela
va nous être des plus pratiques !
Dans ces quelques lignes, on peut remarquer plusieurs tokens. Il est plus facile de les repérer car
ils sont justement mis en valeur par un changement ou non de style de la police dans Delphi :
- "//Formatage du message" est un commentaire, en bleu marine
- "procedure", "string", "var", "begin" et "end" sont des mots réservés, en gras
- "1.5" est un nombre à virgule flottante, en bleu marine
- "10" et "13" sont des nombres, en bleu marine
- " 'Message ID: %f' " et " '%s' " sont des chaines de caractères, en bleu marine
- "AfficherMessage", "IdMsg", "Integer", "Texte", "Sng" (...) sont ce qu'on appelle des symboles, leur police ne change pas
Tous les éléments cités ci-dessus, dans un lexer de code Delphi, seront chacun considérés comme
un token... Ainsi, le groupe de mots entre apostrophes ('Message ID...') est considéré comme une
entité, un unique élément qui sera identifié comme une chaine de caractère. Le token, comme dit au-dessus, est donc
l'ensemble des caractères constituant cette entité.
Il convient bien sûr de définir dans le lexer chaque type de token pouvant être rencontré, car dans
le cas contraire "procedure" ou même "Message" seraient considérés comme des symboles au même titre
que "AfficheMessage".
Le travail du lexer va donc être d'identifier chaque token du texte donné afin de permettre une
analyse de celui-ci. Il est alors évident que chaque lexer sera spécifique à une structure particulière...
Essayez de compiler du VisualBasic dans Delphi s'il vous faut un exemple concret pour comprendre pourquoi !!
Les tokens du VB, du C/C++ (...) ne sont pas ceux de Delphi, et ces derniers ne sont pas également ceux d'un
document au format RTF !
Il reste maintenant à voir comment, dans un texte donné, un lexer va pouvoir extraire chaque
token, en déterminer le type, et ce que peux en faire une application.
II-3. Fonctionnement et utilisation
II-3.a. Analyse descendante
Voici une analyse descendante, qui reste assez générale, du processus de parsing d'un document
afin de montrer les interactions entre le lexer et l'application qui l'utilise.
Cette analyse sera ensuite commentée afin de mieux décrire le fonctionnement
et les actions de chaque partie.

En rouge les actions de l'application, en bleu celles du lexer
Voilà pour une vue "globale" du mécanisme... On pourrait presque dire que l'application n'a rien à
faire, si ce n'est traiter ce qu'on lui donne. Mais justement ce traitement peut s'avérer relativement
compliqué... tout est fonction des données analysées !
Quant au lexer, il a plus de choses à effectuer, et elles peuvent s'avérer assez longues à implémenter,
suivant là encore les données à analyser.
Reprenons donc les points de l'analyse ci-dessus, en les détaillant :
II-3.b. Côté lexer
Je vais me répéter mais il n'y aura pas de code dans ces deux sous-paragraphes, donc
ne vous étonnez pas si les explications ne semblent pas très longues. L'implémentation qui suit
peut l'être beaucoup plus !
Le lexer reçoit du texte à analyser
Ici, il n'y a pas grand chose à faire... Le lexer doit copier le texte fourni
dans un buffer et initialiser un pointeur sur le premier caractère de ce dernier.
Dans la classe TParser de classes.pas, c'est à la création du lexer que l'on passe un Stream en
paramètre (Le TParser étant bel et bien un lexer !). Ce stream contient les données à analyser et le lexer l'exploitera directement en extrayant dans un
buffer les données à analyser.
Rechercher le prochain token
Pour trouver un token, le lexer va tout d'abord passer tous les caractères
considérés comme des espaces (tabulations, espaces... cela dépend du texte analysé) jusqu'à arriver
sur un premier caractère reconnu. En fonction de ce caractère, il va continuer à avancer dans le texte
tant qu'il trouvera des caractères "corrects" pour le token en cours.
Par exemple dans le cas de code Delphi, s'il trouve le caractère '$', il va avancer dans le texte tant
qu'il trouvera des nombres hexadécimaux ([0..9], [a..f] et [A..F]). Le lexer obtiendra ainsi le nouveau
token : un nombre hexadécimal.
Déterminer le type du token trouvé
Cela se fait assez "naturellement"... En fonction du premier caractère trouvé
la recherche effectuée pour avoir le token complet montre que l'on "sait" quel type de token on renvoit. Dans
l'exemple précédent, le lexer pourra immédiatement renvoyer un type "nombre hexa".
Cela est vrai, certes, pour un nombre ou un commentaire qui commence par "//" ou "{", mais si l'on tombe
sur un symbole, c'est-à-dire un mot, c'est différent : un mot réservé, un nom de variable, de classe ou
de fonctions sont tous quatre des symboles, mais on peut vouloir les distinguer... L'astuce ici est
par exemple d'avoir un tableau contenant tous les mots réservés reconnus afin d'en identifier un éventuel.
En règle générale une deuxième "analyse" sur le token trouvé sera nécessaire, mais elle peut à la limite être effectuée
côté application.
Renvoyer le token et son type
Une fois le token trouvé et son type, il suffit au lexer de les renvoyer à l'application...
Rien de plus simple si la recherche se faisait dans une fonction du lexer appelée par le programme !
On est cependant confronté à un problème : une fonction ne peut renvoyer qu'un seul type... donc soit un record,
soit un integer, soit une string (...) ! Pour palier à cela, le TParser de classes.pas ne renvoit en réalité que
le type du token trouvé, et fournit différentes fonctions (TokenInt, TokenString...) qui permettent
de récupérer le token trouvé dans le type désiré.
II-3.c. Côté application
Je ne vous ferai pas l'affront de vous expliquer ce qui doit se passer pour
la création et la libération du lexer ! Il n'y a donc qu'un point à détailler :
Traitement en fonction du token et de son type
Ce traitement dépend complètement des données analysées...
Dans le cadre d'une interprétation de code, par exemple, cela peut-être d'évaluer une condition
après le renvoi d'un token "if" et la récupération du code de cette condition.
Dans le cas de Delphi et de ses DFM (pour lesquels est utilisé TParser de Classes.pas) il s'agirait
de créer les composants correspondant aux noms de classes renvoyés par le lexer, puis d'assigner
les bonnes valeurs aux propriétés de cet objet.
Pour un dernier exemple on peut considérer un lexer de lignes de calculs : si le lexer
renvoit un nombre suivi d'un opérateur et d'un autre nombre, il faut avoir stocké le premier nombre et
effectuer l'opération demandée avec le second nombre.
Les traitements peuvent donc être très variés, et ce sera au développeur de savoir ce
qu'il veut faire des données qu'il reçoit, le tout étant de bien réagir à chaque type de token !
III. Quand utiliser un lexer ?
Maintenant que nous avons expliqué le fonctionnement d'un lexer, nous sommes en droit
de nous demander quand est-ce que cela peut servir ! Effectivement, bien que ce soit au final utile et
"puissant", un lexer est un outil assez lourd à mettre en place et il n'est donc pas question d'en
utiliser à tout va !
Toutefois il ne devrait pas être trop compliqué de répondre à la question "quand" après ce descriptif
des lexers. En effet, si vous avez un fichier de données organisées de telle sorte que ces dernières peuvent
être récupérées dans un ou plusieurs records, nul besoin de développer un lexer !
Par contre, si vous avez un fichier contenant du texte structuré comme un fichier XML, HTML,
du code et j'en passe, là, suivant ce que vous voulez en faire, l'utilisation d'un lexer peut être justifiée.
L'essentiel est de bien définir ce dont vous avez besoin... Parfois vous pourrez peut-être
"contourner" l'emploi d'un lexer par une autre méthode, d'autre fois cette solution se présentera comme la plus
efficace. A titre d'exemple, les utilisations les plus courantes des lexers sont principalement l'interprétation de script,
la compilation de code, l'exécution de calculs et la coloration syntaxique.
Conclusion
Voilà donc cette première partie terminée ! J'espère qu'elle vous aura aidé à bien (ou mieux)
comprendre ce qu'est un lexer et à déjà vous faire une idée de la manière dont un tel outil peut être employé.
Il reste maintenant à en implémenter un afin d'expérimenter l'idée et pour, tant qu'à faire,
satisfaire notre irrésistible besoin de code !
Je vous donne donc rendez-vous pour la prochaine partie de ce tutoriel, dans lequel nous réaliserons
de A à Z une application de coloration de code HTML !
Si vous avez des suggestions quant à cet article, je vous invite à m'en faire part en
m'envoyant un MP.
Nota
Une première version de cette article était intitulée "Parsers 1/2 : Théorie". Ce changement de titre et, de manière plus
générale, le remplacement de "parser" par "lexer" dans le texte de l'article ont été effectués par souci de précision par rapport au sujet.
En effet, un parser est bien un "analyseur", mais c'est le lexer qui a la charge de découper un texte en tokens, ces derniers étant par
la suite réutilisés par le parser pour créer un arbre de parsing du document.
Références
Voici quelques liens qui vous permettront d'approfondir vos connaissances sur le sujet :
Remerciements


Copyright © 2004 Olivier Lance. Aucune reproduction, même partielle, ne peut être faite
de ce site et de l'ensemble de son contenu : textes, documents, images, etc
sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.