HEMDANI
Demande du
token suivant
Programme Analyseur Analyseur
source lexical syntaxique
Token
L’analyseur lexical peut être vu comme une fonction qui à chaque fois quelle est ap-
pelée par l’analyseur syntaxique, lit le programme source caractère par caractère jusqu’à
ce quelle identifie le token suivant qu’elle retourne alors à l’analyseur syntaxique.
La lecture commence avec le caractère suivant la fin du dernier token reconnu et ce
termine avec la reconnaissance du plus long préfixe de la séquence de caractères restante
qui constitue un symbole (une unité lexicale) du langage.
Les symboles seront décrits par des expressions régulières et on utilisera des auto-
mates d’états finis simples et déterministes pour les reconnaître.
1
Abréviations
e+ ≡ e e∗ : l’itération positive
e? ≡ e|ε : l’option
en ≡ e e ... e : n fois e
[ab] ≡ a|b : un caractère parmi plusieurs
[a − c] ≡ a|b|c : une plage de caractères
Remarque 1
En compilation, les expressions régulières seront utilisées pour décrire les symboles
d’un langage de programmation dont les programmes ne sont rien d’autre qu’une suite
de caractères du code ASCII. De ce fait, nous considérerons toujours comme alphabet Σ
l’ensemble des caractères du code ASCII. 2
d1 = e1
d2 = e2
..
.
dn = en
où (di )i=1..n sont des noms distincts et ei une expression régulière sur l’ensemble de
symboles Σ ∪ {d1 , . . . , di−1 }.
Exemple 1
1. Une définition régulière pour les identificateurs peut être :
lettre = A| · · · |Z|a| · · · |z
chif f re = 0| · · · |9
ident = lettre ( lettre | chif f re )∗
chif f re = [0 − 9]
chif f res = chif f re+
partie_f ractionnaire = ( chif f res ) ?
exposant = ( ( E | e ) [+−]? chif f res ) ?
num = chif f res partie_f ractionnaire exposant
2
var
nom:string;
la sous chaîne ”nom” est un lexème filtré par le modèle lettre(lettre|chif f re)∗ du token iden-
tificateur souvent abrégé par id, ou ident. En général, quand l’analyseur lexical reconnaît
un lexème filtré par le modèle lettre(lettre|chif f re)∗ , il retourne un token id à l’analyseur
syntaxique. 2
Exemple 3
Des exemples de tokens et lexèmes correspondants sont donnés dans la table sui-
vante :
Token Lexèmes
ID i, x1, prix_total, . . .
NUM 2, 0.25, 1.5E − 4, −13, . . .
LIT ERAL ”erreur lexicale !”
OP +, −, . . .
CM P <, >, <=, . . .
Les différents tokens rencontrés dans la plus part des langages de programmation
sont :
— les mots clés,
— les opérateurs,
— les identificateurs,
— les constantes (numériques, booléennes, ...),
— les chaînes littérales,
— les séparateurs (parenthèses, virgule, point-virgule, ...),
— ...
Remarque 2
Les tokens représentent les symboles terminaux de la grammaire qui engendre le
langage source. 2
Exemple 4
Les tokens et attributs associés à la déclaration en langage C suivante :
f loat x = 1.5E2 ;
sont
< REEL, > , < ID, “x”> , < ASSIGN, > , < N U M, 150 > et < P V, > 2
3
4 Reconnaissance des tokens
Comme les tokens sont décrits par des expressions régulières (leurs modèles), on uti-
lisera des automates d’états fini simples et déterministes pour les reconnaître.
Remarque 3
1. Par souci d’efficacité, il faut coder les tokens et les codes d’attributs par des nombres
entiers qu’il est préférable de définir comme des constantes ou dans des types énu-
mérés.
2. Pour faire en sorte qu’il n’y ait pas de transitions à partir d’un état final, on procède
comme suit :
Remplacer chaque transition de la forme :
action i
si a sj
par :
si a sj
=a sk reculer
action i
a2 sj2
an sjn
4
par :
si a1 sj1
an sjn
autre sk reculer
action i
Exemple 5
Soit le mini-langage dont la syntaxe est décrite par la grammaire :
P −→ P ;P |I
I −→ if (E == E) I | id = E | ε
E −→ E op E | (E) | id | num
Un DFA reconnaissant les différents tokens et incluant les actions aux niveaux des
états d’acceptation serait :
5
reculer
s7 retourner<NUM, valeur>
s8 reculer
re
autre
aut
retourner<IF, >
c
s3
s1 l|c
f autre
retourner<PV, > s9 reculer
s6 s2 l=f|c autre retourner<ID, nom>
c s4
i
; l=i l|c
’’
s0 = =
s5 s10 retourner<EGAL, >
0 au
tre
+|−
s15
( ) s11 reculer
retourner<FIN, > s12 retourner<ASSIGN, >
s14
retourner<PARD, >
retourner<PARG, >
6
Algorithme 1 Simulation d’un DFA.
si il y a des transitions à partir de l’état alors
lire le caractère suivant ;
si il existe une transition sur ce caractère alors
réaliser la transition /* modifier l’état */
sinon
appeler la procédure de gestion d’erreurs
si non /* l’état est final */
exécuter les actions associées à l’état
Exemple 6
Nous donnons dans cet exemple un programme en langage C simulant le DFA de
l’exemple 5. La codification des caractères est donnée par la table suivante.
autre désigne tout caractère différent de tous ceux qui l’ont précédé ; c’est en fait tout
caractère illégal.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
union {
char * nom;
int valeur;
enum CODEOPERATION cop;
} attribut; /* cette variable contiendra la valeur
d’attribut du dernier token reconu.
*/
7
/* La fonction car_suivant() retourne le code du prochain caractère du
programme source et incrémente la variable position. C’est cette fonction
qui assure la codification des caractères.
* /
int car_suivant(){
int cc; /* caractère courant */
*/
char* get_lexeme(){
int longueur=position-debut; /* longueur du lexème */
char* lexeme=(char*) malloc(longueur+1); /* réservation de l’espace
mémoire.
*/
strncpy(lexeme,programme+debut,longueur); /* le lexème commence à
l’adresse programme+debut.
*/
lexeme[longueur]=’\0’;
return lexeme;
}
8
/* token_suivant() est la fonction princiaple de l’analyseur lexical; c’est
elle qui assure la simulation du DFA. A chaque appel, elle retourne le
prochain token dont elle place la valeur d’attribut dans la variable
attribut.
* /
enum TOKEN token_suivant(){
int cc;
int etat;
etat=0;
while (1){
switch (etat){
case 0: debut=position;
cc=car_suivant();
switch(cc){
case 0: etat=15;
break;
case 1: etat=4;
break;
case 2: etat=2;
break;
case 3: etat=4;
break;
case 4: etat=1;
break;
case 5: etat=5;
break;
case 6: etat=12;
break;
case 7: etat=14;
break;
case 8: etat=13;
break;
case 9: etat=6;
break;
case 10: break;
default: erreur_lexicale();
}
break;
case 1: cc=car_suivant();
while (cc==4)
cc=car_suivant();
etat=7;
break;
case 2: cc=car_suivant();
switch(cc){
case 3: etat=3;
break;
case 1:
case 2:
case 4: etat=4;
break;
default: etat=9;
}
break;
case 3: cc=car_suivant();
switch(cc){
case 1:
case 2:
case 3:
9
case 4: etat=4;
break;
default: etat=8;
}
break;
case 4: cc=car_suivant();
while (cc>=1 && cc<=4)
cc=car_suivant();
etat=9;
break;
case 5: cc=car_suivant();
if (cc==5) etat=10;
else etat=11;
break;
case 6: return PV;
case 7: reculer();
attribut.valeur=atoi(get_lexeme()); /* calcul de la valeur */
return NUM;
case 8: reculer();
return IF;
case 9: reculer();
attribut.nom=get_lexeme();
return ID;
case 10: return EGAL;
case 11: reculer();
return ASSIGN;
case 12: if(get_lexeme()[0]==’+’) attribut.cop=PLUS;
else attribut.cop=MOINS;
return OP;
case 13: return PARD;
case 14: return PARG;
case 15: return FIN;
}
}
}
printf("Taper un programme:\n");
printf("------------------\n\n");
scanf("%[\40-\176]", programme);
position=0;
while (tc=token_suivant()){
switch(tc){
case PV: printf("<PV, >\n");
break;
case IF: printf("<IF, >\n");
break;
case EGAL: printf("<EGAL, >\n");
break;
10
case ASSIGN: printf("<ASSIGN, >\n");
break;
case OP: printf("<OP, ");
if (attribut.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",attribut.nom);
break;
case NUM: printf("<NUM, %d>\n",attribut.valeur);
}
}
printf("<FIN, >\n");
return 0;
}
/*
Exemples d’exécution
====================
Exemple 1
=========
Taper un programme:
------------------
if (x1=max) y=ifs+13-max
Exemple 2
=========
Taper un programme:
------------------
if (x1*2==max) y=ifs+13-max
11
<PARG, >
<ID, x1>
position 7: le caractère ’*’ est illégal!
*/
2
Remarque 4
Le programme est téléchargeable à partir du site web du module. 2
On utilise une table bidimensionnelle delta indexée par les états non finaux de l’auto-
mate et les caractères de l’alphabet d’entrée. Les états et les caractères sont alors codés
par des entiers consécutifs.
Construction de la table
Les entrées de valeur −1 construites par l’algorithme, correspondent à des cas d’er-
reurs ; i.e., des transitions non définies.
Exemple 7
Reprenons l’automate de l’exemple 5 dont la codification des caractères est donnée
dans l’exemple 6. On donne ci-après la table de transition construite par l’algorithme 2.
0 1 2 3 4 5 6 7 8 9 10 11
0 15 4 2 4 1 5 12 14 13 6 0 -1
1 7 7 7 7 1 7 7 7 7 7 7 7
2 9 4 4 3 4 9 9 9 9 9 9 9
3 8 4 4 4 4 8 8 8 8 8 8 8
4 9 4 4 4 4 9 9 9 9 9 9 9
5 11 11 11 11 11 10 11 11 11 11 11 11
Algorithme d’analyse
En utilisant la table de transition, l’analyse lexicale se fait tel que décrit par l’algo-
rithme 3.
12
Algorithme 3 Algorithme d’analyse lexicale.
etat := 0;
tantque etat 6∈ F ∪ {−1} f aire
si etat = 0 alors
debut := position;
f insi;
cc := car_suivant();
etat:=delta[etat, cc];
f ait
si etat ∈ F alors
exécuter les actions associées à etat
sinon /* etat = −1 */
appeler la procédure de gestion d’erreurs
Exemple 8
Pour l’implémentation en langage C de l’analyseur lexical du mini-langage considéré
dans l’exemple 5, il suffit de reprendre le programme donné dans l’exemple 6 et
1. d’ajouter la définition de la table de transition comme suit :
int delta [6][12]={ { 15, 4, 2, 4, 1, 5, 12, 14, 13, 6, 0, -1 },
{ 7, 7, 7, 7, 1, 7, 7, 7, 7, 7, 7, 7 },
{ 9, 4, 4, 3, 4, 9, 9, 9, 9, 9, 9, 9 },
{ 8, 4, 4, 4, 4, 8, 8, 8, 8, 8, 8, 8 },
{ 9, 4, 4, 4, 4, 9, 9, 9, 9, 9, 9, 9 },
{ 11, 11, 11, 11, 11, 10, 11, 11, 11, 11, 11, 11 }
};
etat=0;
while(etat !=-1 && etat<=5){
if (etat== 0) debut=position;
cc=car_suivant();
etat=delta[etat][cc];
}
switch (etat){
case 6: return PV;
case 7: reculer();
attribut.valeur=atoi(get_lexeme());
return NUM;
case 8: reculer();
return IF;
case 9: reculer();
attribut.nom=get_lexeme();
return ID;
case 10: return EGAL;
case 11: reculer();
return ASSIGN;
case 12: if(get_lexeme()[0]==’+’) attribut.cop=PLUS;
else attribut.cop=MOINS;
return OP;
13
case 13: return PARD;
case 14: return PARG;
case 15: return FIN;
default: erreur_lexicale(); /* etat = -1 */
}
}
Le programme obtenu donne exactement le même résultat que celui donné dans
l’exemple 6. Le fichier source complet est téléchargeable en tant que tel à partir du site
web du module. 2
Remarque 5
L’avantage de l’utilisation d’une table de transition est que l’accès à l’état suivant
( delta[etat, cc] ) est rapide. Son inconvénient est que la table peut contenir trop de cas
de transitions indéfinies (matrice creuse) et dans ce cas, la table occupe trop d’espace
mémoire. 2
0 1 2 m
Delta
0
0
1
1
2
2
n
m
Etat Valide
Exemple 9
Considérons la table de transition bidimensionnelle suivante :
-1 2 -1 3 -1
-1 4 1 -1 -1
-1 -1 -1 -1 1
-1 -1 4 -1 -1
14
0 1 2 3 4 5 6 7
Delta -1 2 4 3 4 1 1 -1
0 -1
1 0
0 0 2 3
1 3 3 0
2 2 4 1
3 0 5 1
6 2
Etat 7 -1
Valide
2
Les mots réservés sont traités comme des identificateurs spéciaux : à chaque fois
qu’un identificateur est reconnu, cette table est consultée pour déterminer s’il s’agit d’un
mot réservé. Si tel est le cas, le token correspondant est renvoyé, si non il s’agit donc d’un
identificateur.
Remarque 6
Pour accélérer la recherche, la table est triée par ordre alphabétique des mots réservés
et la recherche est dichotomique. 2
15