Professional Documents
Culture Documents
Il
LANGAGES INFORMATIQUES
Il
www.bibliomath.com
II Table des matières
Chapitre 5 156
Analyse lexicale 156
1 Introduction 156
2 Différents modes de travail d'un analyseur lexical 159
3 Unités lexicales, modèles et lexèmes 160
4 Classes de lexèmes 161
5 Technique de bufferisation 167
6 Modèles de spécification 169
7 Reconnaissance des entités lexicales 172
8 Génération automatique d'analyseurs lexicaux 192
9 Table des symboles 208
10 Traitement des erreurs lexicales 221
Chapitre 6 223
Analyse syntaxique 223
1 Introduction 223
2 Eléments théoriques de base 226
3 Quelques méthodes d'analyse syntaxique déterministe 241
4 Traitement des erreurs syntaxiques 276
5 Table des symboles vue par l'analyse syntaxique 284
6 Exercice récapitulatif 286
Chapitre 7 289
Traduction 289
1 Introduction 289
2 Formes intermédiaires 290
3 Génération de code machine cible 324
Conclusion 335
Bibliographie 336
Index 344
www.bibliomath.com
Avant-propos
Ce livre est une synthèse issue de plusieurs années d'enseignement des modules
de compilation et de théorie des langages. Il est facile à lire, car il est le fruit
d'une longue expérience pédagogique de l'auteur. Il s'adresse principalement aux
étudiants en informatique.
Il résume l'essentiel des concepts de la théorie de la modélisation syntaxique,
et fait une synthèse des méthodes et techniques de compilation.
Outre les nombreux exemples illustratifs permettant de clarifier chaque
nouvelle notion étudiée, on y trouve également des séries d'exercices d'application
avec leurs corrigés.
L'idée qui prévaut est de rendre les notions de compilation agréables à lire en
s'appuyant sur des exemples pédagogiques et réfléchis.
L'ouvrage est organisé en sept chapitres, dont les trois premiers sont consacrés
entièrement à la description des notations et formalismes (grammaires, automates,
etc.) issus de la théorie des langages. Le quatrième chapitre donne un aperçu
général sur les compilateurs et traducteurs. Le reste des chapitres (les trois
derniers) est dédié à l'étude des techniques d'analyse et de traduction.
Plus précisément, le premier chapitre constitue un rappel nécessaire pour
introduire le lecteur dans l'ambiance des langages formels. D'emblée, l'accent a été
mis sur certains aspects, comme par exemple la nécessité d'établir un lien entre la
notion de dérivation et l'analyse syntaxique. Ce lien permet en quelque sorte de
projeter le lecteur dans le contexte de l'analyse syntaxique avant même d'avoir
étudié cette notion. Par ailleurs, il a été mentionné implicitement que seuls deux
types de langages intéressent les compilateurs et les traducteurs de manière
générale, à savoir, les langages réguliers et les langages à contexte libre. Le
chapitre se termine par une série d'exercices avec leurs corrigés.
Le deuxième chapitre a pour finalité de sensibiliser le lecteur sur l'intérêt de
connaitre, voire de maitriser, les automates finis et leurs modèles équivalents : les
expressions régulières. Ce chapitre est indispensable pour maitriser la notion
d'automate fini, élément moteur de l'analyse lexicale. Ce chapitre aussi se termine
par une série d'exercices corrigés.
Le troisième chapitre s'intéresse aux grammaires à contexte libre et aux
automates à pile, ainsi qu'à leurs variantes : les graphes syntaxiques et les réseaux
d'automates. Il aborde quelque peu la notion d'analyse syntaxique. En effet, à
travers les différents exemples proposés, cette notion y est fortement présente.
www.bibliomath.com
2 Avant-propos
Pour clarifier les notions étudiées, une série d'exercices corrigés a été également
ajoutée à la fin du chapitre.
Le quatrième chapitre est une introduction à la compilation qui s'étend sur
plusieurs aspects des systèmes informatiques : de l'historique des compilateurs
jusqu'aux outils d'aide à la construction d'analyseurs, en passant par divers autres
systèmes traducteurs comme les interprètes ou les assembleurs. Le chapitre
présente également les différentes phases et parties d'un compilateur. On a
particulièrement insisté sur l'architecture qui consiste à scinder un compilateur en
deux parties majeures dites partie frontale et partie finale. La partie frontale
regroupe les phases d'analyse, la partie finale regroupe les phases de synthèse.
Le cinquième chapitre est dédié à l'analyse lexicale qui constitue la première
phase de tout système traducteur. Outre les algorithmes de manipulation des
automates finis, les techniques de reconnaissance d'entités lexicales et les
méthodes d'accès à la table des symboles, y sont également décrites sur la base
d'exemples commentés et adaptés.
Le sixième chapitre s'intéresse à la deuxième phase du processus de
compilation, à savoir, l'analyse syntaxique. On s'est limité aux méthodes d'analyse
syntaxique déterministes. Ces dernières sont fondées sur des sous-classes
privilégiées de grammaires à contexte libre.
Enfin, le septième et dernier chapitre est consacré à la traduction qui
représente la partie finale du processus de compilation. On a scindé cette partie en
deux volets principaux, à savoir, la traduction en code intermédiaire et la
traduction en code cible. On a particulièrement insisté sur la traduction en code
intermédiaire. La traduction en code cible a été à peine abordée.
www.bibliomath.com
Chapitre 1
Rappels sur les langages formels
1 Définitions préliminaires
www.bibliomath.com
4 Chapitre 1
Soit l'alphabet V2 = {a, b}. L'ensemble V2• contient toutes les chaines
constituées des lettres a et b, y compris la chaine vide e. Cet ensemble est
également infini dénombrable. Un aperçu sur la liste de ses éléments est : e, a,
b, aa, ab, ba, bb, aaa, aab, aba ... ={a, bt, avec n;:: O.
www.bibliomath.com
Rappels sur les langages formels 5
2 Langages et grammaires
10, 11}.
www.bibliomath.com
6 Chapitre 1
L+ = L1 u L2 u ... = ui
L, alors L*= L+ u Lo et L+ = L L*= L*L.
;~1
Reflet miroir
On définit l'opération miroir de L par LR = {w J w = vR avec v E L}; vR est
le reflet miroir ou mot miroir de v.
. . s1. v = al ... an, a1ors vR = an ... a1.
Alns1 1
Complémentarité
On définit le complémentaire d'un langage L par Le= {w E v* J w e L}.
Différence
La différence de L1 et L2 est le langage noté Ll - L2 constitué des mots
appartenant à Ll et n'appartenant pas à L2. L1 - L2 = {W E y* 1 W E L1 et
w e L2}·
Par exemple, si l'on reconsidère L1 = {aa, bb, ab, ba} et L2 = {ab, bb, ba}, on
aura:
www.bibliomath.com
Rappels sur les langages formels 7
Remarque 2.1
Le symbole a est appelé membre gauche et ~ le membre droit de la règle de
production. Par ailleurs, si a possède plusieurs alternatives (~ 1 , ... , ~n: membres
droits) comme, par exemple, a ~ ~ 1 ; a ~ ~2 ; a ~ ~3·... a ~ ~n, on simplifie
cette écriture en utilisant la barre verticale « 1 » ; on écrira dans ce cas : a ~ ~ 1 1
~2 1 ~3· .. 1 ~n· Le symbole « 1 » indique alors un choix.
Type 3:
• Régulière à droite : Si toutes ses règles de production sont de la forme :
A ~ aB ou A ~ a, avec a E V T*, A et BE V N.
• Régulière à gauche : Si toutes ses règles de production sont de la forme :
A ~ Ba ou A ~ a, avec a E V T *, A et BE V N.
Type 1 : Si toutes les règles sont de la forme a~ ~. sachant que lal :s; l~I avec
a E v*vNv* et ~ EV+. En d'autres termes, on peut aussi avoir ÀAo ~ Àyo,
avec A E VN, À, 0 E v* et 'Y E v+. Mais, pour permettre à ce type de
grammaire de générer le mot vide (lorsque le langage engendré par cette
grammaire contient le mot vide), on introduit l'exception S ~ e, mais l'axiome
S ne doit apparaitre dans aucun membre droit des autres règles.
www.bibliomath.com
8 Chapitre 1
Remarque 2.2
Dans la suite de ce chapitre, on emploie le terme grammaire, toujours pour
désigner une grammaire hors contexte (ou à contexte libre) ou une grammaire
régulière. On utilisera également indifféremment les termes production et règle,
pour désigner une règle de production.
www.bibliomath.com
Rappels sur les langages formels 9
Cette grammaire génère l'ensemble des nombres binaires purs. Les mots 0 et 1
sont obtenus par des dérivations directes. En effet, on a bien S => 0 et S => 1, car
il existe les règles de production : S ~ 0 et S ~ 1.
Les chaines OOOlS et 10011 sont obtenues par des dérivations indirectes. En
effet, on a:
s =>os=> oos => ooos => OOOlS
S => lS => lOS => lOOS => lOOlS => 10011
A chaque pas => (dérivation directe), une règle de production est appliquée.
Avec la dérivation indirecte S =>* OOlS, la règle S ~ OS est appliquée trois fois
consécutivement, à savoir, S => OS => OOS => OOOS ; ensuite, c'est la règle S ~ lS
qui est appliquée, ce qui donne finalement OOOS => OOOlS. Le Tableau 1 illustre la
même démarche pour la deuxième dérivation, c'est-à-dire S =>* 10011.
www.bibliomath.com
10 Chapitre 1
:-----~i
1 N: n_r
G)
:OUT
www.bibliomath.com
Rappels sur les langages formels 11
Par exemple, la dérivation de l'exemple précédent est à gauche (on dit aussi
dérivation gauche, par abus de langage), car toutes les dérivations sont exécutées
à partir de la gauche. Quelle que soit la règle appliquée, elle doit concerner
toujours la règle la plus à gauche (d'où l'appellation Leftmost derivation en
Anglais), comme c'est visible dans la séquence de dérivations suivante :
E => T => F * T => a * T => a * F => a * (E) => a * (T + E) => a * (F + E) =>
a* (a+ E) =>a* (a+ T) =>a* (a+ F) =>a* (a+ a).
Dérivation droite (Right derivation)
Contrairement à la dérivation gauche, avec une dérivation droite, toutes les
dérivations s'exécutent à partir de la droite.
Par exemple, on pourra reprendre le cas précédent et procéder aux dérivations
à partir de la droite pour la chaîne "a + a". On aura la succession de dérivations
suivante:
E => T + E => T + T => T + F => T +a=> F +a=> a+ a
On voit très clairement comment on prend, à chaque pas, le symbole non-
terminal le plus à droite (Rightmost derivation), et le dériver.
On peut aussi reprendre l'exemple "a* (a+ a)" et procéder par dérivation droite.
On aura : E => T => F * T => F * F => F * (E) => F * (T + E) => F * (T + T) =>
F * (T+ F) => F * (T + a) => F * (F + a) => F * (a+ a) =>a* (a +a)
www.bibliomath.com
12 Chapitre 1
www.bibliomath.com
Rappels sur les langages formels 13
que la dérivation gauche d'une chaîne possède pour inverse sa dérivation droite.
Le chemin emprunté lorsqu'on effectue une dérivation par la gauche, n'est pas
nécessairement l'inverse de celui qu'on aurait emprunté lorsqu'on effectue la
dérivation par la droite. D'ailleurs, lorsqu'on a dérivé la chaîne "a + a", on a
obtenu 7t1 = 1 4 5 2 4 5 qui est différente (:;t:) de l'image miroir (1tr) = 5 4 5 4 2 1.
On verra sous peu, à travers un autre exemple, qu'il y a certaines considérations
qui entrent en jeu, qui font que 7t1 n'est pas forcément égale à l'image miroir de 1tr.
Par ailleurs, il faut noter que la dérivation canonique gauche 7t1 représente la
trace d'une analyse descendante. En revanche, la dérivation canonique droite 1tr
n'est pas l'image directe d'une analyse ascendante, mais plutôt son image miroir.
On donnera, à cet effet, un exemple pour lever toute équivoque sur cet aspect qui
est fondamental dans le contexte des analyseurs syntaxiques.
Pour élucider la question concernant la relation entre les dérivations gauche et
droite, et stratégies d'analyse, on se base sur les dérivations canoniques 7t1 et 1tr
obtenues ci-dessus.
Pour vérifier que la chaîne "a + a" appartient effectivement à L(G), la
dérivation canonique gauche 7t1 = 1 4 5 2 4 5 utilise la séquence de dérivations
suivante:
(1) E => T+E
(4) T+E => F+E
(5) F+E => a+ E
(2) a+ E => a+T
(4) a+T => a+F
(5) a+F => a+a
On voit bien que la dérivation gauche coïncide exactement avec l'analyse
descendante puisque le chemin suivi pour faire apparaître la chaîne "a + a" n'est
autre que la dérivation canonique gauche 7t1 = 1 4 5 2 4 5.
Pour vérifier que la chaîne "a + a" appartient effectivement à L(G), la
dérivation canonique droite 1tr = 1 2 4 5 4 5 utilise la séquence de dérivations
suivante :
(1) E => T+E
(2) T+E => T+T
(4) T+T => T+F
(5) E+a => T+a
(4) T +a => F+a
(5) F+a => a+a
Effectuer une analyse montante d'une chaine en utilisant une grammaire,
revient à chercher à réduire cette chaine à l'axiome de la grammaire. Le
Tableau III illustre cette démarche qui est basée sur le principe bien connu
décaler/réduire (shift/reduce) qui utilise une pile pour stocker les résultats
intermédiaires au cours de la phase d'analyse.
En ce qui concerne la colonne nommée Action, soit il y a une règle à
appliquer (Règle numéro i), auquel cas, la réduction correspondante a lieu au
www.bibliomath.com
14 Chapitre 1
niveau de la pile, soit il y a l'action Empiler (x) qui permet de stocker le symbole
x (qui doit être aussi le caractère lu de la chaîne courante Chaine), dans la pile
représentée par Pile.
Lorsque dans la pile en question apparaît le membre droit d'une règle de
production dont le numéro (Règle i) est dans la colonne Action, il va falloir
procéder à une réduction, c'est-à-dire appliquer la règle de production, indiquée
par ce numéro, en sens inverse.
L'utilisation de E sur la deuxième colonne Chaine indique l'absence de
symbole. Autrement dit, la chaîne a été complètement lue et que, par conséquent,
il n'y a plus de décalage à effectuer ; mais, il peut y avoir d'éventuelles réductions,
et ce, jusqu'à la fin de l'analyse.
Ar+;, e Pile
Empiler (a) a+a -
Règle 5 +a a
Règle 4 +a F
Empiler(+) +a T
Empiler (a) a T+
Règle 5 E T+a
Règle 4 E T+F
Règle 2 E T+T
Règle 1 E T+E
Stop E E
www.bibliomath.com
Rappels sur les langages formels 15
A cette issue, on peut affirmer sans détour que l'image miroir de la dérivation
canonique droite correspond exactement au chemin (en termes de numéros de
règles) emprunté par l'analyse ascendante.
r <E - - - - - - - - Racine
Nœud A
interne--------/->\"'-. b _-~'::
_ _ ; .. Feuilles
a d ./.:"''
www.bibliomath.com
16 Chapitre 1
VT = {a, +, * , (, )}
P = {S ~ S + S 1 S * S 1 (S) 1 a}
et les mots "a* a" et "a* (a+ a)" du langage L(G).
Les arbres syntaxiques correspondant respectivement à ces deux mots sont
montrés dans la Figure 3.
a a
*
a * a + a
Remarque 2.5
Si l'on tente maintenant de construire l'arbre syntaxique associé au mot
"a+ a* a", on constate que ce dernier possède plus d'un arbre de dérivation. La
Figure 4 montre qu'il y a effectivement deux manières distinctes de générer le
mot 11 a + a * a". Donc, deux arbres de dérivations distincts sont construits pour
reconnaitre un même mot. Ceci prête évidemment à confusion, car cela signifie
qu'il y a deux chemins différents à suivre pour dériver un même mot. Ce problème
est connu sous le nom de l'ambiguïté de la grammaire G en théorie des langages.
www.bibliomath.com
Rappels sur les langages formels 17
conditionnelle définie par les rêgles {S~if b then S else S 1 if b then S 1 a}.
Cette grammaire est ambigüe, puisque la phrase if b then if b then a else a,
possêde deux arbres syntaxiques (1) et (2) comme illustré par la Figure 5.
(1) s
if b then s else s
s if
~s \a
b then
~s
1
(2) a
if b th en
if b then s el se s
1 1
a a
L'ambiguïté vient du fait qu'un else peut être associé à deux différents then.
On peut lever l'ambiguïté en décidant arbitrairement qu'un else soit toujours
rattaché au dernier then comme (2) de la Figure 5. Dans ce cas, on introduit S1
et S2 de telle sorte que S2 produise toujours l'option if-then-else, tandis que S1
est libre de générer l'une ou l'autre des deux options (if-then ou if-then else).
On obtient alors la grammaire non-ambigüe dont les rêgles se présentent comme
suit:
S1 ~ if b then S2 else S1 1 if b then S1 1 a
S2 ~ if b then S2 else S2 1 a
Même s'il n'existe pas d'algorithme général qui détermine si une grammaire est
ambiguë, il est possible de reconnaitre certaines formes de rêgles de production
qui conduisent à des grammaires ambiguës, comme par exemple S ~ SS 1 a qui
possêde deux arbres de dérivation distincts. En effet, la chaine SSS peut être
générée par deux arbres de dérivation distincts comme sur la Figure 6.
www.bibliomath.com
18 Chapitre 1
Remarque 2.7
Si l'on suppose qu'il n'existe aucune grammaire non ambigüe qui engendre un
langage, l'ambigüité sera dite intrinsèque, c'est-à-dire qu'elle est inhérente au
langage.
www.bibliomath.com
Rappels sur les langages formels 19
Il est souvent commode de supprimer les e-productions qui sont des règles de
la forme A ~ E dans une grammaire à contexte libre. Mais, si le langage L(G)
n'est pas e-libre, c'est-à-dire que L(G) contient e (la chaine vide), alors il est
impossible de ne pas avoir la règle S ~ e.
www.bibliomath.com
20 Chapitre 1
Pour éliminer les E-productions dans une grammaire G = (VN, VT, P, S), on
applique généralement la procédure suivante :
Remplacer toute production A ~ exBp E P par les règles A ~ exBp 1 exp et
B ~ "{1 l···I 'Yn E P, sachant que B ~ E 1 "{1 1... 1 'Yn E P. Les chaines ex et P E v*
et "{1 ... "fn E V+.
Si S ~ E, on introduit un nouvel axiome S' tel que S' ~ S 1 E.
Par exemple, soit G = ({S, A}, {a, b}, P = {S ~ aAb; A~ E 1 aAb}, S}).
On élimine les E-productions selon la procédure précédente. On obtient alors
l'ensemble P = {S ~ aAb 1 ab et A ~ aAb 1 ab} qui est sans E-règles. Ainsi,
Gl = (VNl, VT1, Pi, S1) où VNl = VN, VT1 = VT, P1 = {S ~ aAb 1 ab ; A~ aAb 1
ab} et S1 =S.
Remarque 3.1
Gl présente des règles redondantes. En effet, les membres droits des règles
A ~ aAb 1 ab sont une copie conforme des membres droits des règles
S ~ aAb 1 ab. Cela implique tout simplement que A = S. On peut donc éliminer
cette redondance inutile en remplaçant, soit A par S ou S par A. On aura donc, la
grammaire réduite Gr représentée, soit par S ~ aSb 1 ab, soit par A ~ aAb 1 ab.
Dans le premier cas, Gr= ({S}, {a, b}, {S ~ aSb 1 ab}, S), S est l'axiome de Gr,
et il est l'unique symbole non-terminal.
Dans le deuxième cas, Gr = ({A}, {a, b}, {A ~ aAb 1 ab}, A), c'est A qui est
l'axiome de Gr, et il est l'unique symbole non-terminal.
Un autre exemple de suppression des E-productions, mais avec un langage non-
e-libre, c'est-à-dire qui contient E, autrement dit, un langage dont la grammaire
possède la règle S ~ E pour l'axiome. Soit alors la grammaire représentée par les
règles S ~ aSb 1 Sb 1 E. En appliquant la même procédure que précédemment, on
obtient d'abord les productions S ~ aSb 1 Sb 1 ab 1 b 1 e. Ensuite, pour éliminer S
~ E, il va falloir introduire un nouvel axiome S' tel que S' ~ S 1 E. On obtient
finalement S' ~ S 1 E et S ~ aSb 1 Sb 1 ab 1 b. Cette grammaire est dite E-libre
(sans E-règle) malgré la présence de la règle S' ~ E . Cette dernière est inévitable
(car le langage n'est pas E-libre ), mais elle n'est pas gênante pour autant, puisque
S' n'apparait pas au niveau des autres règles de production.
et soit "ha" une chaine à analyser par cette grammaire. On suppose que l'on
effectue cette analyse en adoptant une stratégie descendante, et que l'on impose
www.bibliomath.com
Rappels sur les langages formels 21
un ordre dans lequel doivent être utilisées les différentes règles. On suit l'ordre
dans lequel apparaissent ces règles dans G. On aura dans ce cas :
La règle S ~ aS n'est pas satisfaisante, et du coup, on change d'alternative,
car aS commence par le caractère "a" qui ne coïncide pas avec le premier
caractère "b" de la chaine "ba".
Le changement d'alternative consiste à essayer S ~A.
Remarque 3.2
Les grammaires cycliques ou non-E-libres sont plus difficiles à manipuler que leurs
homologues acycliques et ë-libres. De plus, dans bon nombre de situations, les
symboles inutiles augmentent incontestablement la taille de l'analyse. Ainsi, tout
au long de cet ouvrage, on suppose que l'on travaille avec des grammaires sans
symboles inutiles, acycliques et ë-libres.
www.bibliomath.com
22 Chapitre 1
www.bibliomath.com
24 Chapitre 1
www.bibliomath.com
Rappels sur les langages formels 25
F ~(SC 1 a
c ~)
Il existe une procédure alternative permettant d'obtenir la forme normale de
Greibach pour une grammaire, sans passer nécessairement par une grammaire non
récursive à gauche. Cette procédure peut également être utilisée pour transformer
une grammaire en son équivalente sans récursivité à gauche.
Pour montrer comment fonctionne cette procédure, on utilise, à titre
d'exemple, les productions suivantes :
A~ AOB 11
B ~ OA 1 BAl 1 0
qui peuvent être réécrites sous forme de système d'équations comme suit :
A= AOB ® 1
B = OA ® BAl ® 0
En utilisant la notation matricielle, on peut réécrire ce système comme suit :
A ~ lX 1 OAZ 1 OZ 1 1
B ~ lY 1OAT1OT1OAi0
X~ OBX 1 OB
Y~OBY
Z ~ lXWZ 1 OAZWZ 1 OZWZ 1 1WZ
W~l
T ~ lXWT 1OAZWT1 OZWT l lWT 11xw 1OAZW1ozw11w
Cette grammaire est sans récursivité à gauche et sous forme normale de Greibach
(FNG).
La FNG permet de construire facilement un automate à pile directement à
partir de la grammaire. L'automate à pile sera étudié au chapitre 3.
4 Exercices
Exercice 4.1
Soient Li, L2 et L3 trois langages. Démontrer les propriétés suivantes :
1- Li.Li=!= Li non idempotence.
2- Li.L2 =!= L2.Li non commutativité.
3- Li.(L2.L3) = (Li.L2).L3 associativité.
4- Li.(L2UL3) = Li.L2uLi.L3 distributivité de la concaténation / union.
5- Li.(L2nL3) =!= Li.L2nLi.L3 non distributivité de la concaténation /
intersection.
6- Montrer que L* = (L *) *
Solution
1°/ Avec un contre-exemple; Ll ={a}; Ll.Ll = {aa} =fa {a} et Ll ={a}. Donc, la
concaténation des langages n'est pas idempotente.
2°/ Avec un contre-exemple; Ll = {a} ; L2 = {b} ; Ll.L2 = {ab} =fa
L2.Ll = {ba}. La concaténation des langages n'est pas commutative.
3°/ Ll.L2.L3 = {xyz 1 x E Ll et y E L2 et z E L3}
Ll.(L2.L3) = {xyz 1 x E Ll et yz E (L2.L3)}
(Ll.L2).L3 = {xyz 1 xy E (Ll.L2) et z E L3}. Par conséquent, la
concaténation est associative.
4°/ Soit w E Ll.(L2uL3) Ç:=> 3 xy t.q w = xy et x E Ll et y E L2 ou y E L3 Ç:=>
(x E Ll et y E L2 ou x E Ll et y E L3) Ç:=> w = xy E Ll.L2 ou w = xy E Ll.L3 ;
w E (Ll.L2uLl.L3). Donc la concaténation est distributive par rapport à l'union.
5°/ Il faut un contre-exemple; Ll ={a, aa} ; L2 ={a} ; L3 = {aa}
L1.(L2nL3) = {a, aa}.0 = 0 (propriété d'absorption de l'ensemble vide) ; alors
que Ll.L2 n Ll.L3 = {aa, aaa} n {aaa, aaaa} = {aaa} =fa 0
6° / Par définition L* = L0 + L1 + L2 + ·.. = Ui;,o L1,. donc L s;;;; L*. Par conséquent,
on a bien L* s;;;; (L *)*
www.bibliomath.com
28 Chapitre 1
Exercice 4.2
Montrer par récurrence sur la longueur du mot v ou w que ( vw)R = wRvR. On
rappelle que vR est le reflet miroir de v.
Solution
Si lvl = 0, alors v = E, et donc, on a : ( vw)R = (ew)R = wR = wR ER = wR vR.
Si lvl = 1, alors v =a, et donc, on a: (vw)R= (aw)R= wRa= wR aR = wR vR.
On ~up~ose à présent que la relation est vérifiée pour lvl = n, c'est-à-dire (vw)R
=W V
Soit x = av c'est-à-dire que lxl = n + 1 ;
On écrit alors (xw)R = (avw)R = (vw)Ra = wRvRa = wR~aR = wR(av)R = wRxR,
C.Q.F.D.
Exercice 4.3
Soit G = (VN, VT, P, S) une grammaire. Indiquer son type, calculer L(G) et
donner la dérivation pour un mot x, pour chacune des grammaires définies comme
suit:
S ~ aSa 1 bSb 1 c x = "abbacabba"
S ~ aSa 1 bSb 1 aa 1 bb x = "abbbbbba"
S ~ A 1 AS, A ~ a 1 b 1 c x = "abbcba"
S ~ aS 1 bS 1 cS 1 a 1 b 1 c x = "bbcacc"
S ~ CSA 1 CDc ; cA ~ Be ; B ~ A ;
D ~ b; bA ~ bDc; C ~a; x = "aaabbbccc"
S ~ RT i E; R ~ aRA 1 bRB i e; AT~ aT; BT~ bT; Ba~ aB;
Bb ~ bB; Aa ~ Aa; Ab ~ bA; aT~ a; bT~ b; x ="baabaa".
Solution
1°/ Type 2;
S => aSa => abSba => abbSbba => abbaSabba => abbacabba ;
L (G) = {wcwR / w E {a, b}*}.
2°/ Type 2;
S => aSa => abSba => abbSbba => abbbbbba ;
L (G) = {wwR / w E {a, b}+}.
3°/ Type 2;
S => AS => aS => aAS => abS => abAS => abbS => abbAS => abbcS => abbcAS =>
abbcbS => abbcbA => abbcba;
L (G) ={a, b, c}+.
www.bibliomath.com
Rappels sur les langages formels 29
4°/ Type 3;
S => bS => bbS => bbcS => bbcaS => bbcacS => bbcacc;
L (G) ={a, b, c}+.
5°/ Type 1 ;
S => CSA => CCSAA => CCCDcAA => CCCDBcA =>CCCDBBc => CCCbBBc =>
CCCbABc => CCCbDcBc => CCCbDcAc => CCCbDBcc => CCC bbBcc => CCC
bbAcc => CCC bbDccc => CCCbbbccc => aCCbbbccc => aaCbbbccc => aaabbbccc;
L (G) = {aibici 1i::::1}.
6°/ Type 0;
S => RT => bRBT => baRABT => baaRAABT => baaAABT => baaAAbT
=> baaAbAT => baabAAT => baabAaT => baabaA T => baabaaT => baabaa;
L (G) = {w.w 1 w e {a, b}*}
Exercice 4.4
Supprimer les symboles inutiles dans la grammaire G = (VN, VT, P, S) définie par
P = {S ~A 1 B; A~ aB 1 bS 1 b; B ~AB 1 Ba 1 aA 1 b; C ~AS 1 b}.
Solution
On remarque d'emblée que le symbole C est inaccessible, car apparaissant comme
membre gauche dans C ~AS 1 b, mais n'apparaissant pas à droite dans les autres
règles ; ce qui fait qu'il n'a aucun lien avec les autres productions. Donc, il est
considéré comme un symbole inaccessible et, de ce fait, il devient inutile de garder
les règles C ~ AS 1 b, le concernant. On obtient donc la grammaire sans symboles
inutiles G' = (VN, VT, P, S) définie par P = {S ~ A 1 B ; A ~ aB 1 bS 1 b;
B ~AB 1Ba1aA1 b}.
Exercice 4.5
Soit la grammaire G = (VN, VT, P, S) dont des règles sont dans l'ensemble P
suivant:
P = {S ~Sa 1Ab1 a; A~ Sa 1Ab1 e}.
1- Supprimer les e-productions dans G.
2- Supprimer la récursivité à gauche de la grammaire obtenue en 1.
3- Rendre la grammaire trouvée en 2 sous forme normale de Greibach.
4- Rendre la grammaire trouvée en 2 sous forme normale de Chomsky.
Solution
1° / La suppression des e-productions suppose que l'on doit remplacer les symboles
X qui donnent e (X~ e) pare dans toutes les règles de production concernées.
On a alors A ~ Sa 1 Ab 1 e, qui devient A ~ Sa 1 Ab 1 b, et la règle A ~ e sera
remplacée dans la règle S ~ Ab ; ce qui donne le nouvel ensemble de règles P'
suivant:
P' = {S ~ Sa 1Ab 1b 1 a ; A ~ Sa 1 Ab 1 b }.
www.bibliomath.com
30 Chapitre 1
S ~ Ab 1 b 1 a 1 AbB 1 bB 1 aB
B~ajaB
S ~ Ab 1 AbB 1 b 1 a 1 bB 1 aB
Les règles de S deviennent après remplacement de A et b dans S ~ Ab 1 AbB
comme suit:
www.bibliomath.com
Rappels sur les langages formels 31
La grammaire sous FNG est G" = (VN, VT, P", S), VN = {S, A, B, C, X, Y}
et l'ensemble des règles P" est :
S -7 bXY 1aXY1bBXY1 aBXYI bY 1bXCY1aXCY1bBXCY1aBXCY1
bCYlbXYBlaXYBlbBXYBlaBXYBlbYBlbXCYBlaXCYBlbBXCYB
laBXCYBlbCYBlblalbBlaB
A -7 bX 1aX 1 bBX 1 aBX 1 b 1 bXC 1 aXC 1 bBXC 1 aBXC 1 bC
B-7alaB
C -7 b 1 bXI bBX 1 bC 1 bXC 1 bBXC
X-7 a
Y-7 b
4°/ Grammaire trouvée en 2 sous forme normale de Chomsky (FNC). Pour être
sous FNC, une grammaire doit avoir ses productions sous la forme A -7 BC ou
A -7 a. Si L(G) contient la chaine vide E, c'est-à-dire S -7 E, il ne faut pas que S
apparaisse à droite dans les membres droits des autres règles. Il s'agit donc de
transformer les règles suivantes :
S -7 Ab 1 b 1 a 1 AbB 1 bB 1 aB
B-7a1 aB
A -7 ba 1aa 1 bBa 1 aBa 1 b 1 baC 1 aaC 1 bBaC 1 aBaC 1 bC
C -7b1ba1bBa1bC1baC1 bBaC.
www.bibliomath.com
32 Chapitre 1
S ~ AY 1 b 1 a 1 AT 1 YB 1 XB
Y~b
x~a
T~YB
B~alXB
A ~ YX 1 XXI YE 1 XE 1 b 1 YF 1 XF 1 YG 1 XG 1 YC
E~BX
F~xc
G~BF
C ~ b 1 YX 1 YE 1 YC 1 YF 1 YG.
Exercice 4.6
1- Montrer que G = (VN, VT, P, S), définie par les règles S ~ aSb 1 Sb 1 b, est
une grammaire ambiguë.
2- Calculer le langage L(G).
Solution
1°/ Pour montrer qu'une grammaire est ambiguë, il suffit d'avoir un mot qui peut
être dérivé de deux manières différentes ou possédant deux arbres syntaxiques
distincts.
En effet, le mot abbb peut être dérivé comme suit :
S => aSb => aSbb => abbb
S => Sb => aSbb => abbb
Ces dérivations correspondent respectivement aux deux arbres syntaxiques
distincts suivants :
s s
/1~
a /s, b A
s b /s~ b
a \ b
1
s .......
b b
2° / Calcul du langage L(G).
Intuitivement, en considérant les règles S ~ aSb 1 Sb 1 b, on remarque que le
nombre de lettres "b" est supérieur, d'au moins une unité, au nombre de lettres
"a". Aussi, le nombre de lettres "a" peut être nul, et le nombre de lettres "b" est
toujours positif puisqu'on a au moins la règle S ~ b. Ainsi, le langage
L (G) = {ai bi 1 j > i et i ~ O}.
www.bibliomath.com
Chapitre 2
Langages réguliers
1 Grammaire régulière
Définition 1.1 (Grammaire régulière)
Une Grammaire G = (VN, VT, P, S) est dite régulière si ses productions
sont de la forme :
A-HxB 1 a pour régulière à droite, a E VT *, A, BE VN
A~ Ba 1 a pour régulière à gauche, a E VT *, A, BE VN
Cette grammaire est régulière à droite car les non-terminaux (ici S) apparaissent
comme suffixes dans les membres droits des règles. On peut tout aussi voir cette
propriété sur les arbres de dérivation. En effet, le mot "1 2 3 4" de la
Figure 7 (a), possède un arbre qui se développe de manière régulière vers la
droite. Par contre, avec la grammaire G = (VN, VT, P, S) avec :
VN = {S}
VT={0,1}
P = {S ~ Sl SO 1 1 1 O}
1
qui est régulière à gauche, l'arbre de dérivation est dirigé vers la gauche. La
Figure 7 (b), montre un arbre de dérivation du mot "1 1 0" qui se développe vers
la gauche.
La grammaire régulière G' = (VN, VT, Pi, S') engendre les nombres entiers
relatifs, avec ou sans signe.
VN = {S', S}
VT= {O, 1, 2, 3, 4, 5, 6, 7, 8, 9, +, -}
S' représente l'axiome
P1 = {S' - j +s 1 -S 1 OS 1 lS l···I 9S 1 0 1 1 1... 1 9} u P. P étant l'ensemble des
productions de la grammaire définie dans le premier des deux exemples
précédents. G' est également régulière à droite.
www.bibliomath.com
34 Chapitre 2
1
/ ""s/ ""'
2 /s""
3 s
(a) ~ {b)
4
s---------------1 q
0 0
1 {1, 4} 1 1
2 2 {2, 4} 2
3 3 3 {3, 4}
4 0 0 0
Représentation graphique
La représentation graphique sous forme de diagramme de transition est une
notation claire et concise. Elle met en valeur la nature des états de l'automate,
chacun par la notation (état initial, état final ou autre) qui lui correspond. Les
sommets du graphe représentent les états ; les arcs représentent les transitions.
La transition 1 (s, a) = q est graphiquement représentée par un diagramme ayant
pour sommet de départ un cercle annoté par l'état « s », et pour sommet d'arrivée
www.bibliomath.com
Langages réguliers 37
un cercle annoté par l'état « q ». L'arc de transition est la flèche annotée par le
symbole 11 a 11 , allant du cercle « s » au cercle « q ». La Figure 8 illustre la
représentation graphique d'une transition.
En ce qui concerne l'état initial, il est préfixé par une double flèche, comme
sur la Figure 9.
L'état final est noté par un double cercle concentrique comme illustré par la
Figure 10.
Remarque 2.2
Quand un automate possède un état qui est à la fois initial et final, le langage
reconnu par cet automate contient forcément la chaîne vide, c'est-à-dire e E L(A).
La Figure 11 illustre le diagramme d'un état à la fois initial et final.
0, 1
L'état « s » est simultanément initial est final, ce qui reflète la chaine vide du
langage {O, 1} *.
Quant à la flèche, qui boucle sur l'unique et même état « s », elle représente
l'itération réflexive avec les caractères 0 ou 1. Ce qui dénote l'ensemble des
chaines formées de l'alphabet de base {O, 1} dans n'importe quel ordre. En
d'autres termes, cela correspond à l'ensemble {O, 1} +.
Donc, l'automate A reconnait bien le langage L (A) = {O, 1}+ u {e}, qui n'est
autre que le langage {O, 1} *.
www.bibliomath.com
Langages réguliers 39
1, 3, 5, 7, 9
1, 3, 5, 7, 9
Par exemple, soit l'automate dont les transitions sont données par le
diagramme de la Figure 14.
b, e
a
e
a, b
Remarque 2.3
L'automate obtenu est simple, mais il n'est pas déterministe. On verra sous peu
comment le transformer en automate déterministe.
www.bibliomath.com
Langages réguliers 41
a, b
a, b
Figure 16 : Diagramme de transition de la Figure 14 complétement
transformé en automate simple
Automate généralisé
Un automate d'états finis généralisé est déterministe si V (s e S et ro e VT *), on a
lh (s, ro)I ~ 1. Autrement dit, V (s e S et ro e VT*), il y a au plus un seul arc
sortant de l'état s. De plus, V (s e S et ro e VT+) :
Si i.(s, ro) -:f. 0 alors on a nécessairement h(s, E) = 0.
Si i.(s, E) -:f. 0 alors on a nécessairement h(s, ro) = 0.
Ainsi, la fonction de transition est notée par h: SxVT*~p(S).
On donne l'automate généralisé défini \'ar A.= ({sa, st}, sa, {a, b}, {st}, h)
avec la fonction de transition h : Sx{ a, b} ~ {sa, st} qui s'exprime par les valeurs
suivantes:
h (sa, ab) =sr
h (sa, ba) = sr
h (sr, ab) =sr
h (sr, ba) = sr
Cet automate est représenté graphiquement par le diagramme de transition de
la Figure 11.
Remarque 2.4
Il est toujours possible de transformer un automate généralisé en un automate
simple, en passant par l'automate partiellement généralisé. Il faut tout simplement
décomposer les transitions de longueur supérieure à 1, en transitions simples (de
longueur 1), ensuite éliminer les E-transitions, s'il y en a.
L'opération inverse est aussi toujours possible, mais elle ne présente aucun intérêt
pratique.
~8--ab_,_b_a_~ ab, ba
www.bibliomath.com
42 Chapitre 2
On a donc obtenu l'automate simple A = ({so, s1, s2 st}, so, {a, b}, {st}, 1). On
remarque aussi que celui-ci est déterministe, c'est-à-dire V (s E {s0 , s1, s2, st} et
x E {a, b}) on a II{s, x)I ~ 1.
Remarque 2.5
Un automate fini déterministe {AFD) est généralement plus facile à manipuler
qu'un automate fini non déterministe {AFN). Cependant, parfois pour des raisons
d'optimisation de l'espace de stockage de la matrice de transition, l'automate non
déterministe s'avère plus efficace. La minimisation du nombre d'états d'un
automate est aussi une question très sensible et intéressante en pratique. On en
reparlera plus amplement dans ce chapitre au niveau de la partie réservée à cet
effet.
www.bibliomath.com
Langages réguliers 43
so' = {so}
I': S'x VT' ---4 S', c'est-à-dire V ({q} E S' et a E VT')
I'({q}, a)= {I(s, a) E s 1 s E {q}}.
F' = {s E S' 1 s n F :t 0}.
a
b
b
a b
s s, p 0
p
0 s, p
www.bibliomath.com
44 Chapitre 2
a b
s {s, p} 0
~~
Figure 20 : Diagramme de transition déterministe de L
0 1
So {so, s1} {so, s2}
{si, SF} {si}
{s2} {s2, SF}
0 0
Les nouveaux états qui contiennent l'état sp (sF étant un état final dans
l'automate initial du Tableau VIII), sont des états finals dans l'automate
déterministe du Tableau IX.
www.bibliomath.com
Langages réguliers 45
0 1
Sn {so, si} {so, s2}
{so, s1} {so, Si, SF} {so, si, s2}
{so, s2} {so, s1, s2} {so, S2, SF}
{so, s1, SF} {so, Si, SF} {so, si, s2}
{so, s2, sF} {so, s1, s2} {so, s2, sF}
{so, s1, s2} {So, Si, S2 1 SF} {So, Si, S2 1 SF}
{so, s1, s2 , st} {So, Si, S2, SF} {So, Si, S2, SF}
0 1
s A B
A c D
B D E
c c D
D E
E
G G
D
G G
G
0, 1
www.bibliomath.com
46 Chapitre 2
www.bibliomath.com
Langages réguliers 47
b
a
=
Avec un mot de longueur nulle on a initialement (pour 0 ), les classes [S-F] et
[F] qui sont représentées respectivement par les ensembles d'états {1, 2, 3, 4} et
48 Chapitre 2
{O, 5}. On doit calculer les classes pour =1. =2, etc., jusqu'à ce qu'on ait =k = =k+l
(stationnarité) pour toutes les classes.
Calcul des classes pour l'équivalence d'ordre 1 (=1)
I (0, a) = 5 ; I (5, a) = 5
I (0, b) = 1 ; I (5, b) = 4
1 = 4 et 5 = 5.
0 0 Donc, les états 0 et 5 sont équivalents quel que soit le mot
x E =
{a, b} de longueur 1. Par conséquent, ::0 = 1 pour les états 0 et 5.
I (1, a) = 4 ; I (1, b) = 3
I (2 , a) - 2 ,· I(2,b)=5
I (3, a)= 3; I (3, b) = 0
I ( 4, a) = 1 ; I (4, b) = 2
On voit très bien que 2 ::0 3 et I (2, x) ::0 I (3, x) pour tout x E {a, b }, alors
dans ce cas on a 2 1 3. =
= =
De même, on a 1 0 4 et I (1, x) 0 I (4, x) pour tout x E {a, b}, on a alors
1
1 = 4. Cependant, les états 2 et 3 ne sont pas équivalents aux états 1 et 4. D'où,
il faut éclater la classe {1, 2, 3, 4} en deux sous-classes {2, 3} et {1, 4}. On
obtient alors trois classes {O, 5}, {2, 3}, {1, 4}.
=
La classe {O, 5} ne sera pas reconsidérée, car on a déjà ::0 = 1 pour les états 0
et 5. En revanche, il faut continuer avec {2, 3} et {1, 4}, car pour ces états on a
=
bien ::0 -:t. 1. Le calcul donne alors les transitions suivantes :
I (1, a) = 4 ; I (1, b) = 3
I (4, a)= 1 ; I (4, b) = 2
= =
Ce qui implique que l'on a 1 1 4 et I (1, x) 1 I (4, x) pour tout x E {a, b}. Par
conséquent, on a 1 =2 4. Dans ce cas =1 = =2 pour 1 et 4.
De même, on a:
I (2, a) = 2 ; I (2, b) = 5
I (3, a) = 3 ; I (3, b) = 0
= =
Ce qui implique que l'on a 2 1 3 et I (2, x) 1 I (3, x) pour tout x E {a, b}. Par
conséquent, 2 =2 3. On a alors =1 = =2 pour 2 et 3.
En somme, pour toutes les classes calculées, on a: =k = =k+l (stationnarité). Ce
qui satisfait la condition d'arrêt. Chaque classe calculée représente un état de
l'automate minimisé.
En désignant les classes {O, 5}, {1, 4} et {2, 3} respectivement par s0, s1 et s2,
on obtient l'automate Am= ({so, si, s2}, so, {a, b}, {so}, I) où I est donnée par les
transitions suivantes :
I (so, a) = sa ; I (so, b) = s1
I (s1, a) = s1 ; I (si, b) = s2
I (s2, a) = s2 ; I (s2, b) = sa
Le diagramme de transition de l'automate résultant de la minimisation de
l'automate A2 est présenté dans la Figure 25.
Langages réguliers 49
3 Expressions régulières
Les expressions régulières sont un autre modèle d'expression des langages de
type 3. Comme préconisé, on se limite à un rappel de certaines notions
fondamentales et résultats qui sont plus ou moins en rapport direct avec les
automates d'états finis ou les grammaires régulières.
L'expression régulière chaine vide « e »
est représentée par le langage { e}
la grammaire régulière équivalente est représentée par l'unique règle S ~ e
l'automate d'états finis équivalent est donné par le diagramme (i) de la
Figure 26.
L'expression régulière symbole terminal « a »
est représenté par le langage {a}, on dit que « a» dénote le langage {a}
la grammaire régulière génératrice est représentée par l'unique règle S ~ a
l'automate d'états finis équivalent est donné par le diagramme (ii) de la
Figure 26.
L'expression régulière le vide 0
Cette expression régulière désigne le langage vide, c'est-à-dire, un ensemble vide.
On n'utilise pas de diagramme de transition pour sa représentation.
L'expression régulière itération réflexive et positive notée a*
est représentée par le langage {a}* ou {an 1 n ~ O}.
la grammaire régulière génératrice est S ~ e 1 aS ou bien S ~ e 1 Sa.
l'automate équivalent possède un état unique, simultanément initial et final
pour exprimer la chaine e, et un arc en boucle pour exprimer la répétition du
symbole «a». Son diagramme est le numéro (iii) de la Figure 26.
L'expression régulière itération positive a+
est représentée par {a}+ ou {an 1 n ~ l}.
la grammaire régulière est S ~ a 1 aS ou S ~ a 1 Sa
l'automate équivalent est exprimé à travers le diagramme (iv) de la Figure 26.
Remarque 3.1
Par commodité de notation (dans cet ouvrage), pour éviter toute confusion entre
l'union (désignée habituellement par l'opérateur +) et la puissance + de l'itération
a+, on utilise l'opérateur ® pour exprimer l'union (somme) de deux expressions
régulières. On sait très bien que les deux itérations a+ et a* sont liées par la
relation a+= a a*. En effet, d'un point de vue des transitions 1 (s0 , a) = Sf
50 Chapitre 2
~Q
(i)
(ii)
6l
a
a
=>
(iii) (iv)
Par exemple, soit la grammaire dont les règles sont S ~ abS 1 baS 1 ab 1 ba.
Pour se ramener au lemme d'Arden, on factorise ces règles, ce qui donne S ~ (ab
1 ba) 1 (ab 1 ba) S. On peut alors écrire S = (ab®ba) ® (ab®ba) S, dont la
Remarque 3.2
On peut étendre le formalisme des expressions régulières même aux règles de
production des grammaires à contexte libre. On donnera ici, juste un petit aperçu
de cette extension sur un exemple de grammaire à contexte libre. Ci-après
quelques notations pour cette extension.
{} * : représente la répétition 0 ou plusieurs fois.
{} + : représente la répétition 1 ou plusieurs fois.
{} *k : représente la répétition au plus k fois.
Langages réguliers 51
Si l'on considère l'exemple de l'automate AT= ({so, si, sr}, so, {a, b}, {si, sr},
I) avec 1 (so, a) =si; 1 (so, b) =sr; 1 (sr, b) =sr, qui reconnaît L ={a, bn 1 n 2:
1}, la grammaire équivalente (L(AT) = L(G)) est G = ({s0 , sr}, {a, b}, P, s0 )
avec P = {so ~ a 1 b 1 bsr, sr~ b 1 bsr}. Mais, en renommant les symboles de VN,
on obtient l'ensemble des productions P = {Z ~a 1b1 bA; A~ b 1 bA}.
On voit très bien sur ce deuxième exemple que si un état est final simple
comme c'est le cas de si, il n'apparaîtra pas comme élément de VN dans la
grammaire équivalente.
Remarque 4.1
On peut également obtenir une grammaire régulière à gauche équivalente en
partant d'un automate fini.
Pour simplifier la procédure, on applique les points suivants :
Introduire la règle s0 ~ e (so étant l'état initial de l'automate AT).
Pour toute transition 1 (s, a) = q, introduire la règle q ~ sa.
Si l'état final est unique, il devient axiome de la grammaire, sinon introduire
un nouvel axiome Z tel que Z ~ fi 1 f2 ... 1 fn, où fi, f2··.fn, sont des états de F.
Réduire alors la grammaire de sorte à éliminer les e-productions.
Par exemple, on donne l'automate AT = (S, so, VT, F, I) qui reconnait
L = {b, abn 1 n 2: O}. Sa fonction de transition 1 est définie par :
1 (so, a) = f
1 (so, b) = p
1 (f, b) = f
F = {f, p} est l'ensemble des états finals, et s 0 est l'état initial.
En appliquant les points précédents on obtient :
so~e
f ~ soa 1 fb ; p ~ sob
Z ~P If
Par substitution de so ~ e, on obtient les règles Z ~ a 1 b 1 fb et f ~ a 1 fb.
On remarque que la règle p ~ b, a disparu. En effet, puisque la règle Z ~ p, a
été remplacée par la règle Z ~ b, donc la règle p ~ b devient superflue. En
renommant les symboles de V N, on obtient la grammaire régulière à gauche
Langages réguliers 53
G = (VN, VT, P, Z) avec VN = {Z, K}, VT = {a, b}; L'ensemble des règles
P = {Z ~ a 1 b 1 Kb; K~ a 1 Kb} engendre exactement le même langage que
celui de l'automate AT donné en entrée, à savoir, L = {b, abn 1 n ~ O}.
Les états finals deviennent des états non finals. Le nouvel état initial qu'on
notera Sa, sera tel que I (Sa, E) = q, V q E F, F étant l'ensemble des états
finals de l'automate A. On obtient ainsi l'automate AR représenté par le
diagramme de transition de la Figure 29.
Le diagramme de transition de AR de la Figure 29 reconnait donc le langage
miroir de L (G), c'est-à-dire LR (G) = {b, bna 1n2". O}.
Il suffit maintenant de supprimer les E-transitions. Ce qui donne le diagramme
miroir transformé et finalisé représenté par le diagramme de la Figure 30.
0, 1
0
Propriétés fondamentales
(Ro@Ri ... ®Rn) // a= (Ra//a) ® (Rif /a)® ... ® (Rn//a)
(RiR2) // a = (Ri//a)R2 ® <p(Ri).(R2 //a) tel que :
• <p(Ri) = e si le langage représentant Ri contient e
• cp(Ri) = 0 sinon
R *//a= (R//a)R*
R//xy = (R//x)//y
Remarque 4.2
On rappelle que si un langage est fini, il est régulier.
Si le nombre de dérivées d'un langage est fini, le langage est régulier. On
utilise ce résultat qui est une conséquence du théorème de Nerode pour
construire un automate d'états finis en utilisant les dérivées. Le théorème
mentionne que le nombre de dérivées d'un langage régulier (expression
régulière) correspond aux nombre d'états d'un automate d'état finis.
Autrement dit, chaque dérivée est dénotée par un état de l'automate fini.
Ainsi, si on a R//x = Ri (l'expression Ri est la dérivée de R par rapport à x),
alors on peut avoir la transition I (R, x) = Ri, c'est-à-dire, qu'on peut transiter
de l'état représenté par R vers l'état représenté par Ri par le symbole x. C'est
de cette manière que l'on construit pas à pas, l'automate fini, à partir d'une
expression régulière par la méthode des dérivées.
s1//a =(a+ Ef> c)*//a =(a+ Ef> c)//a (a+ Ef> c)* = a*(a+ Ef> c)*= 82 e F, alors
l(8i, a) = 82
sif /c = (a+ Ef> c)*//c = (a+ Ef> c)//c (a+ Ef> c)* = (a+ Ef> c)* = 83 e F, alors
1(8i, c) =
83
s2/ /a = a*( a+ Ef> c) *//a = a*// a (a+ Ef> c ) * Ef> (a+ Ef> c ) *//a = a* (a+ Ef> c)* Ef>
(a+ Ef> c) //a( a+ Ef> c) *= a* (a+ Ef> c) * Ef> a* (a+ Ef> c)* = a* (a+ Ef> c) * = 82 e F, alors
1(82 1 a) = 82
s2/ / c = a* (a+ Ef> c) */ / c = 0 Ef> (a+ Ef> c) */ / c = (a+ Ef> c) / / c( a+ Ef> c) *= (a+ Ef> c) *=
83 1 alors 1(82 1 c) = 83
s3//a =(a+ Ef> c)*//a =(a+ Ef> c)//a (a+Ef> c)*= a*(a+ Ef> c)* = 82, alors
1(83 1 a) = 82
s3//c = (a+Ef> c)*//c = (a+Ef> c)//c (a+Ef> c)*= (a+Ef> c)*= 83 1 alors1(83 1 c) = 83.
On s'arrête de dériver car on retombe sur des dérivées qui ont déjà été calculées.
(i) (ii)
(iii) (iv)
(v) (vi)
4.6 Récapitulation
Un langage fini est régulier. Mais, un langage régulier n'est pas forcément fini.
A titre d'exemple le langage L = {an 1 n ;?: O} est régulier et infini.
Pour tout langage régulier, il existe une grammaire régulière qui le génère.
Pour toute grammaire régulière à gauche, il existe une grammaire régulière à
droite équivalente.
Pour toute grammaire régulière qui génère un langage L, existe un automate
d'états finis qui accepte L. Autrement dit, l'automate et la grammaire sont
équivalents, s'ils représentent le même langage.
Tout langage régulier (automate d'états finis, grammaire régulière) admet une
expression régulière qui le dénote.
Tout automate d'états finis non déterministe admet son équivalent
déterministe.
Tous les langages réguliers sont déterministes. Par conséquent, ils ne sont
jamais ambigus.
5 Exercices
Exercice 5.1
Trouver une grammaire régulière à gauche qui engendre :
1° / les nombres binaires impairs dans le système de numération binaire pur.
2° / les nombres entiers relatifs.
Trouver une grammaire régulière à droite qui engendre :
3° / les nombres entiers naturels divisibles par 2.
4°/ les nombres entiers naturels divisibles par 3.
5° / les nombres binaires divisibles par 2 dans le système de numération binaire
pur.
6° / les nombres binaires dont le nombre de 0 est un multiple 3.
7° / l'ensemble des chaines formées de caractères de l'alphabet {a, b, c} dont le
dernier caractère est apparu au moins 1 fois.
8° / les nombres entiers naturels divisibles par 5.
Langages réguliers 61
Solution
1° / Intuitivement, la grammaire régulière à gauche est décrite par les règles
suivantes:
S ~Al l 1 I Sl
A~ AO 101 SO
On peut aussi construire cette grammaire en s'appuyant sur la procédure qui
utilise l'automate fini qui reconnait les nombres binaires impairs.
On commence d'abord par l'automate, ensuite, on applique la procédure vue en
section 4 du présent chapitre. L'automate en question est représenté par le
diagramme suivant :
0 1
1
0
L'automate miroir est représenté par le diagramme suivant :
1
0
0
S ~ lA l 1B l 1
A~ lAI 1B l 1
B ~ ol oBI oA
qui se réduit en les règles A ~ lA l lB l 1 ; B ~O 1OB 1OA ; A étant l'axiome.
A présent, il ne reste plus qu'à la transformer en grammaire régulière à
gauche pour avoir finalement les règles suivantes :
A~ Al 1Bll1
B ~ 01BO1 AO
Cette grammaire est la même que celle obtenue intuitivement.
2° / Entiers relatifs
S ~Sc 1Ac1 c
A~+l-
Le chiffre c E {O, 1, 2... 9}
On peut aussi utiliser l'automate fini comme avec la question 1. Mais, on laisse
le soin au lecteur d'utiliser cette méthode à titre d'exercice.
a, b, c a
a, b, c
a, b, c
9°/ Soient c un chiffre dans l'ensemble {O, 1, 2, ... , 9}, « e » le caractère qui
symbolise 10 pour les puissances de 10, et le point « . » représentant la virgule
décimale dans l'écriture d'un nombre réel.
La grammaire régulière à droite envisagée pour le 1er cas est décrite par les règles
suivantes:
S ~ +A 1 -A 1 c 1 cB
A~ c 1 cB
B ~.c 1 • 1 e 1 eF 1 c 1 cB
C ~ c 1 cC 1 e 1 eF
F ~ + 1 - 1 +G 1-G 1 c 1 cG
G ~ c 1 cG
Cette grammaire engendre les réels tels qu'ils sont décrits syntaxiquement dans
Turbo PASCAL. A titre indicatif, elle peut générer, par exemple, le nombre 65.e
ou 65.e -. En effet, S => 6B => 65B => 65.C => 65.e et S => 6B => 65B => 65.C =>
65.eF => 65.e -, qui représentent tous les deux, 6.5000000000E+Ol qui est 65x10°.
Pour rappel, le caractère « e » représente le 10 pour les puissances de 10.
Le deuxième cas pour ces nombres est beaucoup plus large puisqu'il accepte même
les nombres qui commencent par une virgule décimale (le cas des nombres < 1).
Mais, les nombres comme 65.e et 65.e -, ne sont pas acceptés dans ce deuxième
cas. La grammaire qui engendre ce type de réels est décrite par les règles
suivantes:
S ~ +A 1-A 1c 1cB 1.C
A~ .cl cl cB
Langages réguliers 65
B -7 c 1cB l .D 1.1 eG
C -7 c 1 cD
D -7 c 1cD 1eG
G -7 c 1cF 1+K 1-K
F -7 c 1cF
K-7c lcF
Exercice 5. 2
Soit l'expression régulière a (a <f> b) * ba (b <f> a)* b
1° / Construire l'automate d'états finis équivalent par la méthode des dérivées.
2° / Confirmer 1° par une construction intuitive suivant des diagrammes de
transition.
3° / En déduire alors la grammaire régulière à droite et la grammaire régulière à
gauche correspondantes.
Solution
1°/ Méthode des dérivées :
a(a<f>b) *ba(b<f>a) *b //a= (a<f>b) *ba(b<f>a) *b =si, alors l(5o, a) = 51.
a(a<f>b)*ba(b<f>a)*b // b = 0, alors 1(50 1 b) = 0.
( a<f>b) *ba(b<f>a) *b / / a = ( ( a<f>b) * / / a) ba (b<f>a )*b <f> ba(b<f>a)*b / / a =
(a<f>b)*ba(b<f>a)*b = s1, alors 1(51 1 a) =51.
(a<f>b)*ba(b<f>a)*b // b = ((a<f>b)* // b) ba(b<f>a)*b <f> ba(b<f>a)*b // b =
(a<f>b)*ba(b<f>a)*b <f> a(b<f>a)*b = s 2, alors 1(51 1 b) = 52.
((a<f>b) *ba(b<f>a) *b <f> (b<f>a) *b) //a = (a<f>b) *ba(b<f>a) *b <f> (b<f>a) *b = s3 ,
1(53, a) = 83.
66 Chapitre 2
Toutes les dérivées (si, s2, s3, s4) ont été calculées ; I(s4, a) = s3 et I(s4, b) = s4
montrent que ce n'est plus nécessaire de continuer car on est retombé sur des
dérivées déjà calculées.
Ci-après, le diagramme de transition correspondant à l'automate fini
déterministe recherché.
a b a b
0 1 0 sO sl 0
1 1 <1, 2>
<1, 2> <1, 3> <1, 2> sl sl s2
<1, 3> <1, 3> <1, 2, 3, 4> s2 s3 s2
<1, 2, 3, 4> <1, 3> <1, 2, 3, 4>
s3 s3 s4
s4 s3 s4
Langages réguliers 67
L'état S4 est un état final car l'état sous-ensemble <1, 2, 3, 4> contient l'état
final 4.
Cette matrice est conforme à l'automate fini déterministe obtenu par la
méthode des dérivées en 1•.
b
=>
<==
a b
D c 0
a b c c <C,B>
<C,B> <C,A> <C,B>
D c 0 <C,A> <C,A> <C,A,B,S>
c c -~g~,1:3_&?__ __<ÇÇ!~_? ___ --~Ç-'-Ail?_,ê_? __
<C, B>
B A 0
D
i a
c
b
0
A A <A, S>
c c B'
s 0 0 B' A B'
A' A' E
E
------------ -
A E
--
- - - - - - -- --- - - - - - - - -·
70 Chapitre 2
b
=>
Exercice 5.3
Montrer par la méthode des dérivées que les langages 11 et 12 ne sont pas
réguliers.
1°/ 11= {Onln 1n ~ 1}.
2°112= {anbmlm > n ~ O}.
Solution
li Démontrer que 1 1 = {Onln 1 n 2 1} n'est pas de type 3.
Il suffit d'appliquer le théorème de Nerode ; donc il faut montrer que le nombre
de dérivées du langage n'est pas fini.
11 = {Onln 1 n 2 1} = {01, 0212, 0313,... } =sa;
L2 //a:
Si n = 0 alors L2 / / a = 0
Si n > 0 alors L2 / / a = {an-l bm 1 n ;;:: 1 et m >1} = s1
L2 // b:
Si n = 0 alors L2 / / b = {bm-l 1 m ;;:: 1} = s2
Si n > 0 alors L2 / / b = 0
s1 //a:
Sin= 1 alors sl //a= 0
Si n > 1 alors sl / / a = {an- 2 bm 1 n ;;:: 2 et m > 1} = S3
S1 // b :
Si n = 1 alors sl / / b = {bm-l 1 m ;;:: 1} = s2
Sin > 1 alors sl // b = 0
s2//a=0
s2 // b = {bm- 2 / m;;:: 2} = S4
s3 // a:
Sin= 2 alors, S3 //a= 0
Si n > 2 alors S3 / / a = { an- 3 bm 1 n ;;:: 3 et m > 1} = S5
s3//b:
Si n = 2 alors S3 / / b = {bm-l 1 m ;;:: 1} = s2
Si n > 2 alors s3 / / b = 0
On obtient donc les dérivées, comme s2, s4 1 etc., qui sont de la forme {bm-i 1 m;;:: i
et i;;:: 1}, ainsi que les dérivées si, s3 1 S5 1 etc., qui sont de la forme {an-i bm 1 n;;::
i et m > 1} avec n ~ oo, m ~ oo, i aussi, donc le nombre de dérivées tend lui
aussi vers l'infini, et donc L2 n'est pas régulier.
Exercice 5.4
On donne Ll = {anc v abc 1 n;;:: 1} et L2 = {cnab v abc 1 n;;:: l}.
Trouver les grammaires régulières et les automates d'états finis pour les langages :
L2c, LluL2 et Ll *.
Solution
L'automate fini complémentaire pour L2 = {cnab v abc 1 n;;:: l}. Le langage
complémentaire est L2c. On dessine d'abord le diagramme de transition de
l'automate qui reconnait L2, ensuite on déduit le diagramme de transition de
l'automate qui reconnait L2c. Enfin, on extrait la grammaire régulière qui
engendre L2c à partir du diagramme de l'automate fini déterministe qui
reconnait L2c. En principe, la procédure d'extraction se passe en deux temps.
On ajoute d'abord un état supplémentaire dit état erreur. Ensuite, pour tout
état de S et tout symbole de VT, soit il existe une transition, soit on ajoute
une transition vers l'état erreur. Enfin, on inverse le statut des états : les états
finals deviennent non finals et les états non finals deviennent finals.
72 Chapitre 2
=>
11:
Transformation i
On constate que l'état T est devenu inaccessible, donc il est inutile de le garder.
D'où, la grammaire qui engendre Ll * qui est décrite par les règles suivantes :
R~aAjaBle
74 Chapitre 2
A~bC
C ~ c 1 cD
B ~ aB 1c1 cD
D~aAjaB
Exercice 5.5
Soit la grammaire de type 3 définie par : A ~ OB 1 lA 1 e; B ~ OC 1 lB;
C ~ OA 1 lC. A étant l'axiome.
1•/ Construire la grammaire régulière à gauche correspondante.
2°/Appliquer le lemme d'Arden pour calculer l'expression régulière
correspondante.
3° / Construire l'automate d'états finis déterministe correspondant.
4•/ L'automate trouvé est-il minimal ? En déduire la grammaire régulière à droite
correspondante.
Solution
1•/ Pour rappel, la grammaire est définie par les règles suivantes :
A ~ OB 1 lA 1 e ; B ~ OC 1 1B ; C ~ OA 1 lC ; A étant l'axiome.
On construit d'abord le diagramme de transition, ensuite on procède à la
construction de la grammaire régulière à gauche selon la procédure qui s'appuie
sur l'automate miroir.
1
Automate pour la =>
grammaire donnée
Automate miroir
obtenu
A~ lA l 11 OC
C ~ lC 1 OB
B ~ 1B 101 OA
La grammaire régulière à gauche est donnée par la grammaire miroir de la
précédente. Elle est représentée par les règles suivantes :
Z ~ Al 1 1 1 CO 1 e
A~ Al l l I CO
C ~Cl 1 BO
B ~ Bl 101 AO
Remarque 1.1
Une Grammaire de type 2 est dite algébrique, hors contexte ou à contexte libre.
Le terme hors contexte vient du fait qu'un non-terminal A, peut toujours être
remplacé par~ E v* au cours d'une dérivation (en utilisant la production A ~ ~),
indépendamment de tout contexte. Dans ce qui suit, on utilise indifféremment les
termes hors contexte ou à contexte libre pour désigner une grammaire de type 2.
Par exemple :
la grammaire qui engendre les expressions arithmétiques simples, correctement
parenthésées, signées ou non, définie par le quadruplet G = (VN, VT, S, P), est
une grammaire hors contexte où l'on a:
VN = {S, E, T, F}.
VT = {i, n, +, -, (, ), *, /}.
Les lettres i et n représentent respectivement un identificateur (nom d'une
variable) et un nombre (constante numérique). S est l'axiome de
G = (VN, VT, S, P), L'ensemble des productions P est décrit par les règles
suivantes :
78 Chapitre 3
S ~ E 1 +E 1-E
E~TIE+FI E-T
T~F 1T*F1 T/F
F ~ i 1n1 (S)
De même, la grammaire qui engendre l'ensemble des expressions booléennes
correctement parenthésées, définie par le quadruplet G = (VN, VT, S, P), est
également une grammaire hors contexte, avec les éléments suivants :
VN= {S, E, T, F}
V T = {a, c, -, , A, v , ( , ) }
Les lettres « a » et « c » représentent respectivement un identificateur
(variable logique) et une constante logique comme vrai et faux ou 0 et 1.
S est l'axiome et les symboles -, , A et v représentent respectivement les
opérateur logiques « non », « et » et « ou ».
L'ensemble des productions P est décrit par les règles suivantes :
S~El•E
E~TITvE
T~FIFAT
F ~a 1c1 (S)
Remarque 1.2
D'autres notations pour les règles de production ont été introduites pour exprimer
d'une autre manière les notations conventionnelles des règles de production.
seront remplacées par les règles sous la forme BNF comme suit :
<S> ::= <A> * <S> 1 <A>
<A> ::= <B> + <A> 1 <B>
<B> ::= (<S>) 1 a
Grammaires hors contexte et automates à pile 79
Input A ~ Output
Input A Output
Output
s
Figure 39: Graphe syntaxique des productions: S -7 B S + B; B -7 B * C;
1
! C ; C -7 a ! (S)
2 Automate à pile
Définition 2.1 (Automate à pile)
Un automate à pile est défini formellement par le 7-uplet
Ap = (s, sa, vT, r, #, F, I) où:
S est l'ensemble des états de l'automate,
so E S est l'état initial de l'automate,
VT est l'alphabet terminal de base de l'automate,
r est l'alphabet de pile,
84 Chapitre 3
~ Tête de lecture
1 1 l l±J[ 1 rn+ ---- Bande de lecture
1
! sommet -- ..
Bloc de contrôle 1
B }
B + - - - Pile
Par état final L (At) = {ffiEVT* 1 (so, ro, #) 1--* (st, E, a)} avec St E F et
aE r *.
Lors de ce mouvement, il y a:
l'automate qui passe de l'état « s » à l'état suivant « p ».
le déplacement de la tête de lecture d'une case vers la droite.
le dépilement du symbole A et l'empilement de a. E r * à la place.
la lettre "a" est le symbole qui force la transition. Pour rappel a E VTu{E}.
Remarque 2.1
Si a.= E, alors il y a dépilement.
Si a. = Z et A = Z, la pile reste inchangée ; Z E r.
Si le symbole "a" qui force la transition est égal à E, la transition sera dite
spontanée, autrement dit, la modification de la pile et le changement d'état se
font sans déplacement de la tête de lecture.
Remarque 2.2
Par commodité de notation, le sommet de pile « A », sur la configuration
(s, am, Aô), est dirigé vers la gauche. Cette orientation vers la gauche est inspirée
de la dérivation gauche avec les grammaires. On verra, plus loin, qu'il existe un
autre type d'automate à pile pour lequel le sommet de pile sera dirigé vers la
droite.
s p q
(ii) :1 q-,
f -- ( :-Y-)-1-1-:-1-1-(-s~-Y-)-----!
s p q
(iii)
: 1--1-(p --+---:--+1-:--11
0-'e_)
La représentation graphique
Tout comme l'automate sans pile, il est également possible de représenter la
fonction de transition d'un automate à pile par un graphe (habituellement
nommé diagramme de transition), dont les sommets et les arcs représentent
respectivement les états et les transitions. La différence, avec l'automate sans
pile, réside dans la façon d'exprimer les transitions. Ainsi, si la transition
I(s, a) = q, de l'automate fini est graphiquement exprimée par le diagramme
de transition (i) de la Figure 41, celle de l'automate à pile, à savoir,
I(s, a, A) = (q, a) est dénotée par le diagramme (ii) de la même figure.
b/# (e)
a/#(#) 26-~26~
q,.<'. 0
* *~
Figure 43 : Diagramme de l'automate à pile (en mode pile vide) de
L = {an bn 1 n ~ 1}
En effet, le mot "aab" est fini d'être analysé, mais la pile n'est pas encore vide.
Donc, le mot en question n'est pas accepté, c'est-à-dire n'appartient pas au
langage L.
Construire l'automate à pile qui accepte le même langage L = {an bn 1 n ~ 1},
mais par critère d'état final.
Tout comme précédemment, on s'appuie sur la construction graphique.
L'automate en question est spécifié par le diagramme de la Figure 44.
b/# (e)
~G )~ )
<Y*:\_)
~
~
alternative, déjà rencontré avec les automates finis, est connu sous l'appellation de
non-déterminisme. C'est un problème entièrement résolu avec les langages
réguliers puisque ces derniers sont tous déterministes. En effet, « Pour tout
automate d'états finis (sans pile}, il existe son équivalent (reconnaissant le
m~me langage) déterministe ».
Les langages algébriques ne sont pas tous déterministes, donc leurs outils
générateurs (les grammaires hors contexte) et leurs homologues accepteurs
(automates à pile), eux non plus, ne sont pas tous déterministes. On verra, plus
loin, qu'il existe dans la famille des langages algébriques, certaines sous-classes de
langages déterministes engendrés par des grammaires un peu spéciales (LL, LR,
précédence, etc.).
En ce qui concerne les règles Z ~ aZb 1 ab, de la grammaire proposée ci-
dessus, il est possible de les remplacer par les nouvelles productions Z ~ aA ;
A ~ aAb 1 b. Ces dernières n'engendrent pas de transitions multi définies,
contrairement aux règles initiales Z ~ aZb 1 ab. En effet, en reconsidérant
l'automate à pile vide sur la base de ces nouvelles règles, on obtient l'automate
Ae = ({so}, so, {a, b}, {Z, A, b}, Z, 0, I) avec une nouvelle fonction de transition
définie par :
1 (so, a, Z) = (so, A)
1 (so, a, A)= (so, Ab)
1 (so, b, A) = (so, e)
1 (so, b, b) = (so, e)
• L'analyse du mot 11 aaabbb 11 engendre les pas suivants :
(so, aaabbb , Z) 1-
(so, aabbb, A) 1-
(so, abbb, Ab) 1-
(so, bbb, Abb) 1-
(so, bb, bb) l-
(so, b, b) l-
(so, e, e)
C'est une configuration de 11 Succès 11 ; le nombre de pas de l'analyse est égal à celui
obtenu avec l'automate à pile vide conçu intuitivement.
• L'analyse des mots 11 abb 11 et 11 aab 11 est donnée par les séquences d'analyse
suivantes:
(so, abb, Z) 1-
(so, bb, A) 1-
(so, b, e)
C'est une situation de blocage car la pile est vide, et le mot n'est pas entièrement
analysé. Donc le mot 11 abb 11 est rejeté par l'automate.
(so, aab, Z) l-
(so, ab, A) i-
(so, b Ab) 1-
(so, e, b)
Grammaires hors contexte et automates à pile 95
L'analyse du mot 11 abb 11 est terminée, mais la pile n'est pas vide. Donc, le mot
"abb", lui non plus, n'a pas été accepté par l'automate.
Remarque 2.4
La reconnaissance d'un langage par un tel automate ressemble à l'analyse qu'on
aurait obtenue par dérivation gauche en utilisant une grammaire hors contexte
équivalente. A ce titre, l'automate sera considéré comme un modèle d'analyseur
gauche (ou descendant) qui est tout le contraire de l'automate à pile étendu que
l'on définira sous peu. Ce dernier réalise une analyse ascendante et, de ce fait, sera
considéré comme un modèle d'analyseur ascendant.
Remarque 3.1
L'automate à pile étendu ne peut pas être construit intuitivement aussi facilement
que l'automate à pile gauche. La construction de ce type d'automate est
généralement basée sur les règles de production d'une grammaire hors contexte.
Une transition d'un tel automate permet :
96 Chapitre 3
Configuration initiale (sa, ro, #) où sa est l'état initial E S, ro est une chaîne à
analyser et # est le symbole du fond de la pile.
Configuration de « Succès » ou d'acceptation de la chaîne (st, e, e) [pile vide et
à état final].
Pour passer d'une configuration donnée à une configuration successeur, il est
nécessaire de satisfaire certaines conditions concernant les éléments du triplet
(s, 'JI, y), à savoir, \;:/ (s E S, a E VTu{e}, y E r*) si (p, a.) E I(s, a, y) alors
(s, am, yô) 1- (p, m, a.ô) avec p E S, a., ô E r *et m E VT *.
Par convention, et pour une meilleure lisibilité, le sommet de pile est dirigé
vers la droite sur la configuration (s, am, yô). Cette orientation vers la droite est
inspirée de la dérivation droite avec les grammaires à contexte libre.
E/ ab (Z)
E/ aZb (Z)
a/ E (a)
b/ E (b)
=>
E/ #Z (E)
8
Figure 4 7 : Automate à pile étendu pour L = {an bn 1 n ~ 1}
Id (sa, a, e) = (sa, a)
Id (sa, b, e) =(sa, b)
Id (sa, e, ab)= (sa, Z)
Id (sa, e, aZb) =(sa, Z)
Id (sa, e, #Z) = (qr, e)
• L'analyse du mot 11 aaabbb 11 génère les pas suivants :
(sa, aaabbb, #) 1- Décaler 11 a 11 (ou Empiler 11 a 11 )
www.bibliomath.com
98 Chapitre 3
L'automate â pile étendu Ad = ({so, Clf}, so, VT, (VNUVT)u{#}, #, {Clf}, Id)
basé sur les productions {S ~ S + T 1 T; T ~ T * F 1 F; F ~ (S) 1 a}, n'est pas
déterministe, car la fonction de transition définie par :
Id (so, x, E) = (so, x) pour tout x E VT ={a, +, *, (,)}
Id (so, E, a) = (so, F)
Id (so, E, (S)) (so, F)
Id (so, E, F) = (so, T)
Id (so, E, T * F) = (so, T)
Id (so, E, T) = (so, S)
Id (so, E, S + T) = (so, S)
www.bibliomath.com
Grammaires hors contexte et automates à pile 99
On a déjà vu, plus haut, que le langage L = {an bn 1 n ;;:: 1} est accepté par un
automate à pile déterministe (modèle d'analyseur descendant). On peut également
construire un automate à pile étendu déterministe (modèle d'analyseur ascendant)
qui accepte L.
Remarque 3.2
La transition E / # Z (E) est une transition un peu spéciale qui ne correspond ni à
un décalage, ni à une réduction ; elle marque tout simplement la fin de l'analyse.
On la remplace par $ / #Z (e) dans le diagramme de la Figure 48 afin d'éviter
une mauvaise interprétation des conditions concernant le déterminisme de
l'automate. En outre, puisque le symbole $ est un marqueur de fin de l'analyse, il
doit forcément être différent des éléments de V T·
• L'analyse du mot 11 aabb 11 génère les pas suivants :
(so, aabb$, #) 1- Décaler 11 a"
(p, abb$, #a) 1- Décaler 11 a 11
(p, bb$, #aa) 1- Décaler "b"
(r, b$, #aab) 1- Réduire "ab" en Z
(t, b$, #aZ) 1- Décaler 11 b 11
(s, $, #aZb) 1- Réduire 11 aZb 11 en Z
(t, $, #Z) 1-
Quand le symbole $ est rencontré, l'automate transite vers l'état final « f »
comme suit:
(f, E, E) Stop
C'est une configuration de « Succès » ; le mot 11 aabb 11 a été accepté.
www.bibliomath.com
100 Chapitre 3
a /E (a)
~ G)1----
Règle Z ~AB
www.bibliomath.com
Grammaires hors contexte et automates à pile 101
Règles A ~ aA 1 a
A:
Règles B ~ bA 1 b
B:
Z:
Les transitions spéciales : comme par exemple, celle qui joint l'état « t » à
l'état « r », à savoir, I(t, Z) = r qui nécessite un traitement un peu spécial
comme son nom l'indique.
En principe, lorsque les transitions sont ordinaires, elles sont directes et
permettent d'effectuer un passage d'un état à l'autre en lisant tout simplement un
symbole de VT· En revanche, les transitions spéciales, nommées également
transitions étiquetées, sont indirectes car le passage effectif d'un état à l'autre, n'a
lieu qu'une fois que le symbole non-terminal E VN (cas de Z sur la Figure 50 avec
la transition I(t, Z) = r) est consommé. On verra sous peu qu'un non-terminal
constitue toujours l'état initial d'un automate du RAF. Consommer le symbole Z,
revient à transiter à l'état final de l'automate représenté par Z. Schématiquement
cela se passe comme dans l'appel et le retour d'une procédure. On résume donc le
processus comme suit :
www.bibliomath.com
102 Chapitre 3
Quand l'automate marqué par Z est à son état final « f », il faut dépiler l'état
« r » empilé précédemment, ensuite transiter vers l'état « r », lui-même. Cette
action est interprétée comme un retour de la procédure appelée. Récupérer
l'adresse sauvegardée avant l'appel, ensuite se brancher à cette adresse, dite
adresse de retour.
En bref, on peut donc concevoir un automate à pile sur la base d'un RAF
comme celui des Figures 49 et 50.
Règles
Z ~ ZC 1 a Z:
Règles
C ~ BC 1 b C:
Règle B ~Ca
Règles
Z ~ ZC 1 a s1 :
Règles
C ~ BC 1 b s4:
Règle B ~Ca
b (s5, s4) = s5
l3 (s1, s4) = ss
ls (ss, a) = sg
La première partie de la fonction de transition associée est obtenue,
conformément à la définition, en s'appuyant sur la liste précédente des fonctions
li, 12 et Is.
1 (s1, e, X) = (s1, s2X) avec X E r
1 (si, a, X) = (s3, X)
1 (s2, e, X) = (s4, s3X)
1 (s4 1 E:, X) = (s1 s5X)
1
1 (s4 1 b, X) = (s5 1 X)
1 (s5 1 E:, X)= (s4 1 s5X)
1 (s7 1 E:, X) = (s4 1 ssX)
1 (ss, a, X) = (sg, X)
Pour compléter la liste, on ajoute les valeurs de la fonction 1 pour tous les
états q E F et r E r - {#}sachant que F = {s3, S5, Sg} et r - {#} = {s2, S3, S5,
s5, ss}. On aura donc 15 nouvelles valeurs de la fonction de transition, listées
comme suit:
e,
1 (s3 1 s2) = (s2 1 e) 1 (s5, e, s2) = (s2 1 e) 1 (sg, e, s2) = (s2 1 e)
e,
1 (s3 1 s3) = (s3 1 e) 1 (s5, e, s3) = (s3, e) 1 (sg, e, s3) = (s3 1 e)
e,
1 (s3 1 s5) = (s5 1 e) 1 (s5 1 e, s5) = (s5, e) 1 (sg, e, s5) = (s5 1 e)
e,
1 (s3 1 s5) = (s5 e)
1 1 (s5 1 e, s5) = (s5, e) 1 (sg, e, s5) = (s5 1 e)
1 (s3 e,
1 ss) = (ss, e) 1 (s5 1 e, ss) = (ss, e) 1 (sg, e, ss) = (ss, e)
C'est une configuration de « Succès », car l'automate a atteint son état final S3 et
le mot "abab" a été complètement analysé, donc accepté.
www.bibliomath.com
Grammaires hors contexte et automates à pile 105
Remarque 4.1
Cet automate n'est pas déterministe, mais on a simulé son comportement de
manière déterministe pour économiser le nombre de pas de l'analyse. On aurait pu
avoir beaucoup plus de pas, si on n'avait pas agi de la sorte.
On ne peut pas supprimer ce non-déterminisme aussi facilement, directement
sur l'automate, car cela est dû à la grammaire utilisée qui, elle-même, n'est pas
déterministe. Il aurait fallu rendre la grammaire déterministe (dans la mesure du
possible) pour prétendre construire un automate à pile déterministe.
La grammaire Z -7 ZC 1 a ; C -7 BC 1 b ; B -7 Ca, utilisée ci-dessus, est
récursive à gauche. La récursivité à gauche constitue déjà un handicap majeur
pour les analyseurs descendants (gauches). Mais, de manière générale, elle n'est
pas toujours la seule raison du non-déterminisme. Ce dernier peut être inhérent au
langage. Donc, quelle que soit la grammaire G, avec ou sans récursivité à gauche,
on ne peut pas toujours obtenir un automate à pile déterministe Ap tel que
L(Ap) = L(G).
L'exemple suivant consiste en la construction d'un automate à pile sur la base
d'une grammaire qui génère des expressions arithmétiques. Cette grammaire est
sans récursivité à gauche et factorisée. La factorisation permet de minimiser le
nombre d'alternatives pour une transition donné de l'automate.
Les règles de production suivantes sont celles d'une grammaire à contexte libre
qui génère un sous ensemble des expressions arithmétiques simples correctement
parenthésées.
E-7 TM
M -7+TM1 E
T-7 FN
N -7 *FN i E
F -7 (E) 1 a
Le RAF associé à cette grammaire est décrit par les diagrammes de transition
de la Figure 53.
Règle E ~TM
Règles
M~ +TM 1 E
M:
Règle T ~ FN
www.bibliomath.com
106 Chapitre 3
Règles ~
N~ *FN / E N:
Règles ~
F ~ (E) 1 a F:
M:
E
l
M:
M: --+
Figure 54: Simplification des diagrammes associés aux règles M --7 +TM 1 e
et E --7 TM
On complète par les valeurs de I lorsque l'automate atteint l'un des états finals
{s2 1 s4 1 ss}. Commer-{#} = {s2, S4 1 s1} 1 alors il y a encore neuf autres valeurs
pour la fonction 1 qui sont listées comme suit :
I (s2, E, s2) = (s2 1 e); 1 (s4 1 E, s2) = (s2 1 e) ; 1 (ss, E, s2) = (s2 1 e);
1 (s2 1 E, s4) = (s4 1 e); 1 (s4 1 E, s4) = (s4 1 e); 1 (ss, E, s4) = (s4 1 e)
1 (s2 1 e, s1) = (s1, e); 1 (s4 1 e, s1) = (s1, e); 1 (sa, E, s1) = (s1, e)
Soit à analyser le mot "a*(a+a)"
www.bibliomath.com
108 Chapitre 3
(s1, a*(a+a), #) 1-
(s3, a*(a+a), s2#) 1-
(s5, a*(a+a), S4S2#) 1-
(ss, *(a+a), S4S2#) 1-
(s4, *(a+a), s2#) 1-
(s3, (a+a), s2#) 1-
(s5, (a+a), S4S2#) 1-
(s5, a+a), S4S2#) 1-
(s1, a+a), S7S4S2#) 1-
(s3, a+a), S2S7S4S2#) 1-
(s5, a+a), S4S2S7S4S2#) 1-
(ss, +a), S4S2S7S4S2#) 1-
(s4, +a), S2S7S4S2#) 1-
(s2, +a), S7S4S2#) 1-
(s1, a), S7S4S2#) 1-
(s3, a), S2S7S4S2#) 1-
(s5, a), S4S2S7S4S2#) 1-
(ss, ), S4S2S7S4S2#) 1-
(s4, ), S2S7S4S2#) 1-
(s2, ), S7S4S2#) 1-
(s1, ), S4S2#) 1-
(ss, e, S4S2#) 1-
(s4, e, s2#) 1-
(s2, e, #)
a/# (#)
+,*/# (#)
( / #(##)
5 Transducteur à pile
Un transducteur à pile est considéré comme un automate à pile (modèle
d'analyseur descendant), muni d'un ruban de sortie. Autrement dit, au lieu de
reconnaitre simplement un langage, il en assure également la traduction. La
Figure 57 illustre schématiquement les différents composants d'un transducteur à
pile.
---,1--rl:::ll±JO::::T-1----.I
.-1 rn.-----
Tête de lecture
Bande de lecture
Î
Bloc de contrôle
sommet --·a
B }._ --- Pile
Tête d'écriture
.- - - - - Bande d'écriture
La configuration initiale, notée (s0 , co, #, E) avec s0 qui est l'état initial, co est
la chaîne à analyser et à traduire. # est le contenu initial de la pile. Enfin,
E représente le ruban de sortie qui est entièrement vierge initialement.
La configuration finale notée (st, E, a., 'JI) où Sf est l'état final d'acceptation de
la chaîne co, a. e [' * est le contenu de la pile à la fin de l'analyse. Si a. = E,
l'acceptation est par pile vide (ou par critère généralisé lorsque Sf est gardé
comme état final parallèlement au mode d'acceptation par pile vide). 'JI est la
chaîne obtenue en sortie à la fin de l'analyse ; elle représente la traduction de
co par le transducteur à pile T p·
www.bibliomath.com
Grammaires hors contexte et automates â pile 111
On peut également avoir les deux modes, par état final et par pile vide
simultanément.
{p, 2 3 6 4 5 1 4 6 2 4 6, Z, E ) 1-
(p, 3 6 4 5 1 4 6 2 4 6, A, E ) 1-
{p, 6 4 5 1 4 6 2 4 6, BA*, E ) 1-
(p, 4 5 1 4 6 2 4 6, aA*, E ) 1-
(p, 4 5 1 4 6 2 4 6, A* 1 a ) 1-
(p, 5 1 4 6 2 4 6, B* 1 a ) 1-
(p, 1 4 6 2 4 6, Z* 1 a ) 1-
(p, 4 6 2 4 6, AZ+*, a ) 1-
(p, 6 2 4 6, BZ+*, a ) 1-
(p, 2 4 6, a Z+*, a ) 1-
(p, 2 4 6, Z+*, aa ) 1-
(p, 4 6, A+*, aa ) 1-
(p, 6, B+*, aa ) 1-
(p, E, a+*, aa ) 1-
(p, e, +*, aaa ) 1-
(p, E, * a a a+ ) 1-
(p, e, E, a a a+* ) 1-
Effectivement, "a a a+*" est la forme post-fixée de l'expression "a* (a+ a)". Le
mode d'acceptation et de traduction réalisée par ce transducteur est par pile vide.
www.bibliomath.com
114 Chapitre 3
6 Exercices
Exercice 6.1
Etendre la grammaire des expressions arithmétiques du premier exemple donné
dans Remarque 1.1 de la section 1 du présent chapitre, afin qu'elle puisse
engendrer également des expressions comportant des exposants. Un exposant
peut être une expression arithmétique ou une fonction comportant une liste
d'arguments. Un argument est une expression arithmétique.
Transformer la grammaire trouvée en son équivalente EBNF.
Solution
Une grammaire hors contexte qui engendre les expressions arithmétiques
correctement parenthésées, signées ou non est définie par le quadruplet
G = (VN, VT, S, P) où :
VN = {S, E, T, F}
VT = {i, n, +, -, (, ), *, /}
Les lettres i et n représentent respectivement un identificateur (nom d'une
variable) et un nombre (constante numérique).
S est l'axiome.
L'ensemble des productions P est décrit par les règles suivantes :
S --7 E J +E J -E
E--7TJE+TJE-T
T--7FJT*FJT/F
F --7 i J n J (S)
On peut facilement modifier cette grammaire si l'on veut qu'elle engendre
également des expressions arithmétiques avec exposant. Il suffit d'ajouter la règle
qui engendre le symbole d'exponentiation. On obtient ainsi la liste supplémentaire
suivante:
F --7 K J K~F
K --7 i J n J (S) J R
R --7 i (L)
L --7 S L, S
J
E--7TJE+TJE-T
T--7FJT*FJT/F
www.bibliomath.com
Grammaires hors contexte et automates à pile 115
F ~
K J K~F
K ~ i 1 n 1 (S) 1 R
R ~ i (L)
L ~ S 1 L, S
Le nouvel ensemble des non-terminaux est VN = {S, E, T, F, K, R, L} où S
est l'axiome.
Exercice 6.2
Construire une grammaire de type 2 qui genere l'ensemble des expressions
logiques. Une expression logique est une expression booléenne ou une expression
relationnelle. On suppose qu'une expression relationnelle est définie par une
relation entre deux expressions arithmétiques. Cette relation est établie par un
connecteur relationnel de comparaison comme >, <, >=, <=, -:f. et =.
Solution
La grammaire des expressions logiques est définie par G = (VN, VT, S, P) avec :
VN= {S, L, T, F, R, E, A, B, C}
VT ={a, n, m, +, - , * , / , ( , ) , = , > , < , >= , <= , -:f.,-,, /\, v}
Les lettres a, m et n représentent respectivement un identificateur (nom d'une
variable) un nombre (constante numérique) et une constante booléenne.
L'ensemble des règles est le suivant :
S ~L l •L
L~TJTvL
T~FJF/\T
F ~
a 1 n 1 (S) 1 ERE
R ~ = 1 < 1 > 1 >= 1 <= 1 *
E~A 1 +A J-A
A~BJB+AJB-A
B~CIC*BIC/B
C ~a 1m1 (E)
Exercice 6.3
Trouver une grammaire de type 2 qui engendre le langage des expressions
régulières.
www.bibliomath.com
116 Chapitre 3
Solution
On peut s'inspirer, par exemple, de la grammaire des expressions arithmétiques
pour construire une grammaire qui engendre le langage des expressions régulières.
A la base, on peut proposer la grammaire « squelette » définie par les règles
de production suivantes :
S -7 S Etl S 1S • S 1(S) 1s+ 1s* qui représentent une grammaire ambiguë. Mais,
cette ambigüité peut être supprimée en introduisant de nouvelles variables non-
terminales. On obtient ainsi la grammaire G = (VN, VT, S, P) décrite par les
règles de production suivantes :
S-7AEtlSIA
A-7B•AIB
B -7 c 1c+1 c*
C -7 (S) 1a 1E 10
VN = {S, A, B, C}
VT ={a, E, 0}
Les symboles E et 0 sont utilisés ici en tant que méta symboles de
longueur= 1. En effet, par exemple dans les chaines a•E ou (aEtle)•0 générées par
la grammaire ci-dessus, E n'est pas pris comme un élément neutre et 0 n'est pas
pris comme élément absorbant. Autrement dit, on se limite tout simplement à la
syntaxe du langage qui permet d'écrire des expressions régulières
indépendamment de toute autre considération.
Exercice 6.4
Construire le graphe syntaxique pour la grammaire qui engendre l'ensemble des
expressions régulières de l'exercice 6.3.
Solution
Graphe syntaxique de la grammaire décrite par les règles de production
suivantes:
S-7BIBEtlS
B-7CIC•B
c -7 D 1n+1 n*
D -7 a 1E 10 1(S)
~
1~+=1
s A
1 i ~NI I /
1 1 1
, B
s
N
1 T
1
®
1 +1 N 1 î 1 /
B (
www.bibliomath.com
Grammaires hors contexte et automates à pile 117
N
c A
D
N
T + /
D
* 1 /
Exercice 6.5
Construire un automate à pile déterministe (modèle d'analyseur descendant) qui
reconnait le langage des expressions régulières de l'exercice 6.4.
Egalement pour la construction de cet automate, on peut s'inspirer de l'automate
qui reconnait les expressions arithmétiques correctement parenthésées. Pour
faciliter la conception, il est conseillé d'utiliser l'approche graphique.
Solution
Comme préconisé, on peut s'inspirer de l'approche de construction graphique. On
obtient ainsi l'automate représenté par le graphe de transition de la figure ci-
après.
(sa, (a*®E)+•0, #) 1-
(sa, a*®E)+•0, ##) 1-
(s1, *@Et•0, ##) 1-
(s2, ®Et•0, ##) 1-
(sa, Et•0, ##) 1-
(s1, t•0, ##) 1-
(si, +.0, #) 1-
(s2, •0, #) 1-
(sa, 0, #) 1-
(si, E, #) Stop
) /#(E)
{}/# (#)
C'est une configuration de « succès ». La chaine a été acceptée, par l'automate qui
a atteint un état final nommé s1. Il est à noter que F = {s1, s2}. Le lecteur peut
tester d'autres mots s'il le désire.
Exercice 6.6
Construire un automate à pile déterministe (modèle d'analyseur descendant) qui
reconnait L = {an bn 1 n ~ O}. Il est conseillé d'utiliser l'approche graphique.
Solution
Un automate à pile déterministe (modèle d'analyseur descendant) qui reconnait le
langage L = {an bn 1 n ~ O} est décrit par le diagramme de la figure suivante :
a/A (AA)
a/# (A#)
b/A (ë)
Grammaires hors contexte et automates à pile 119
C'est un automate à pile avec un mode d'acceptation par état final. On peut
même alternativement construire l'automate à pile vide qui est représenté par le
diagramme suivante :
a/#(#)
b/#(e)~
~
*0
C'est un automate à pile vide défini par le 7-uplet Ae = (S, s0 , VT, r, F, I) où :
s = {so, S2, sa}, So est l'état initial, VT = {a, b}, r = {#}, F = 0. La fonction de
transition 1 est définie par le diagramme ci-dessus. Le lecteur peut également
tester les mots qu'il souhaite en adoptant toujours la même configuration
d'analyse (état, entrée, pile).
Exercice 6. 7
Soit le langage L ={an brn 1 m > net n ~ O}.
Construire un automate à pile déterministe modèle d'analyseur descendant
(intuitivement de préférence) qui reconnait L, en s'appuyant sur la méthode
graphique.
Solution
L'automate déterministe qui reconnait le langage L = {an brn 1m > net n ~ O} est
décrit par le diagramme de transition de la figure suivante. Ce diagramme est
construit intuitivement directement sur la base du langage L.
a/# (A#)
a/A (AA) b/A(e)
b/# (#)
b/A (A)
Exercice 6.8
Construire un automate à pile basé sur un RAF pour le langage
L = {an bn 1 n ~ 1}, en utilisant les règles de production A ~ aAb 1 ab.
120 Chapitre 3
Solution
On construit d'abord le RAF pour le langage L = {an bn 1 n <:: 1}, en utilisant les
règles de production A ~ aAb 1 ab. Le diagramme de transition (ii) de la figure
suivante représente le RAF finalisé qui sert à construire l'automate à pile
envisagé.
A:
·~ (i) : RAF préliminaire
q /-:
q ::/:
A:
Grammaires hors contexte et automates à pile 121
Exercice 6.9
On considère la grammaire des expressions arithmétiques décrite par les règles de
production numérotées suivantes :
E ~TM (l)
M ~+TM (2) 1 E ( 3)
T ~ FN (4)
N ~ *FN (5) 1 E (5)
F ~ (S) (7) 1 a (s)
Construire un transducteur à pile qui accepte toute expression arithmétique
infixée générée par cette grammaire, et produit en sortie sa correspondante post-
fixée.
Pour faciliter la construction il est conseillé vivement d'utiliser un schéma de
traduction dirigée par la syntaxe (STDS).
122 Chapitre 3
Solution
On établit d'abord le STDS sur la base des règles de production et leurs
traductions respectives.
E~TM (1) TM
M~+TM (2) TM+
M~e (3) e
T~FN (4) FN
N ~ *FN (5) FN*
N~e (6) e
F ~(E) (7) E
F~a (8) a
Ensuite, on construit un tandem de transducteurs. Le premier, reçoit en entrée
une expression infixée, et produit en sortie les numéros de règles de production
utilisées. Le deuxième, reçoit les règles produites par le premier transducteur et
génère en sortie la forme post-fixée de l'expression infixée présentée en entrée du
premier transducteur. Ainsi, la fonction de transition du premier transducteur est
définie comme suit :
1 (s, e, E) = (s, TM, 1)
1 (s, +, M) = (s, TM, 2)
1 (s, e, M) = (s, e, 3)
1 (s, e, T) = (s, FN, 4)
1 (s, *, N) = (s, FN, 5)
1 (s, e, N) = (s, e, 6)
1 (s, (, F) = (s, E), 7)
1 (s, a, F) = (s, e, 8) /* Ce premier transducteur n'est pas déterministe */
Celle du deuxième transducteur est définie par :
1 (p, 1, E) = (p, TM, e)
1 (p, 2, M) = (p, TM+, e)
1 (p, 3, M) = (p, e, e)
1 (p, 4, T) = (p, FN, e)
1 (p, 5, N) = (p, FN*, e)
1 (p, 6, N) = (p, e, e)
1 (p, 7, F) = (p, E, e)
1 (p, 8, F) = (p, a, e)
1 (p, e, a) = (p, e, a)
1 (p, e, *) = (p, e, *)
1 (p, e, +) = (p, e, +) /* Ce deuxième transducteur est déterministe */
Analyse de la chaine "a* a+ a" par le premier transducteur
(s, a * a + a, E, e) l-
(s, a * a + a, TM, 1) i-
(s, a * a + a, FNM, 14) l-
(s, * a + a, NM, 148) i-
(s, a + a, FNM, 1485) 1-
Grammaires hors contexte et automates à pile 123
1 Introduction
1.1 Schéma simplifié d'un compilateur
Schématiquement un compilateur est un type particulier de programme
d'ordinateur qui reçoit en entrée un programme informatique écrit dans un
langage source et produit en sortie un autre programme équivalent écrit dans un
langage cible (voir Figure 58).
programme cible
programme source
présenté en entrée
---+~--+ ou programme
1
objet généré en
~
sortie
rapport d'erreurs
2 Variantes de compilateurs
Un compilateur est divisé essentiellement en deux parties clés : l'analyse et la
synthèse.
La partie analyse, comme son nom l'indique, est destinée à analyser les
différentes instructions spécifiées par le programme source et à créer une forme
intermédiaire. Cette dernière pourrait être conservée, par exemple, dans une
structure hiérarchique spéciale nommée arbre abstrait. Un arbre abstrait
décrivant une instruction d'affectation est présenté dans la Figure 59.
La partie synthèse reçoit en entrée la forme intermédiaire précédente et
génère en sortie le code cible correspondant.
Alors qu'un compilateur, ne peut que traduire un langage informatique vers un
autre, la réalisation de programmes, notamment au sein d'équipes nombreuses,
requiert bien d'autres activités qui sont généralement couvertes par un
AGL (Atelier de Génie Logiciel). Il existe une variété d'outils logiciels d'aide au
développement et à la production de programmes informatiques. Certains d'entre
eux effectuent d'abord une certaine forme d'analyse. On cite quelques exemples de
tels outils [Aho, 86) :
Introduction à la compilation 127
.-
/ '\.
y +
/
a
/
* \
b
"' 10
\ documentclass{ minimal}
(1) ----+ \ begin{document}
\[\sum _ {n=lY{ +\infty}\frac{1}{n n2}=\frac{\pi n2}{6}\l
\end {document}
+oo
3 Contexte du compilateur
La Figure 61 décrit un environnement typique de compilation.
En plus du compilateur, la génération d'un programme cible exécutable peut
nécessiter plusieurs autres programmes. En effet, le programme cible en langage
d'assemblage de la Figure 61 généré par le compilateur est traduit au préalable
Introduction à la compilation 129
en code machine translatable par l'assembleur, avant d'être relié avec des routines
de librairies et d'éventuels fichiers objet translatables, pour produire le code qui
sera exécuté directement par la machine (code machine absolu).
Il existe cependant plusieurs environnements de compilation, où les compilateurs
produisent eux-mêmes du code machine translatable, voire même du code
directement exécutable.
Le processus de compilation, comme on l'a déjà souligné plus haut, peut être
divisé en deux parties, l'analyse et la synthèse. L'analyse, dite aussi partie
frontale du compilateur, lit le programme source et génère une représentation
intermédiaire. Cette dernière est compilée à nouveau au niveau de la partie
synthèse dite aussi partie finale du compilateur, qui produit une représentation
cible. La partie finale dépend en général uniquement du langage intermédiaire et
des caractéristiques de la machine cible. L'avantage de la traduction intermédiaire
est qu'elle permet de reprendre la partie frontale d'un compilateur et de réécrire
uniquement la partie finale, si l'on désire construire un compilateur pour le même
langage source, sur une machine cible différente. De même, on peut compiler
plusieurs langages source distincts en le même langage intermédiaire et employer
la même forme intermédiaire pour tous ces langages. On peut ainsi construire
plusieurs compilateurs pour une ou plusieurs machines distinctes. Néanmoins,
cette manière de procéder n'a connu qu'un succès limité à cause, particulièrement,
de différences subtiles dans les principes des différents langages.
La partie frontale comprend l'analyse lexicale, l'analyse syntaxique, l'analyse
sémantique et la production de code intermédiaire.
L'analyse lexicale, dite aussi analyse linéaire ou encore scanning en Anglais, lit
le flot de caractères formant le programme source, de gauche à droite, et le
découpe en unités lexicales (ou unités atomiques : tokens en Anglais) qui sont
des abstractions de lexèmes. Ces derniers sont des mots du langage, c'est-à-dire
les mots-clés, séparateurs, identificateurs, etc. Une unité lexicale (token), par
contre, est une suite de caractères ayant une signification collective. Mais, pour
lever toute équivoque entre ces deux notions on considère l'instruction de
contrôle while (y >= t) y :=y - 3, dont les entités sont collectées dans le
Tableau XII.
Introduction à la compilation 131
Le logiciel qui effectue une analyse lexicale est appelé analyseur lexical ou
scanner. Parallêlement à la tâche de collecte des unités atomiques, l'analyse
lexicale élimine également les espaces blancs, les commentaires et tout autre
caractêre superflu, inutiles pour la suite du processus de compilation.
lexème token
while WHILE
( LPAREN
y IDENTIFIER
>= COMPARISON
t IDENTIFIER
) RPAREN
y IDENTIFIER
.- ASSIGNMENT
y IDENTIFIER
- ARITHMETIC
3 INTEGER
SEMICOLON
'
Tableau XII- Exemple d'un ensemble de paires (lexème, unité lexicale}
<affectation>
<idf>
1
y
----- ---- .- /
/
<exp>
\------
<exp> + <exp>
/ / "-.... 1
<exp>
* <exp> <nbr>
<idf>
1
<i~f> 1
10
1 1
a b
.-
/ '\.
y +
/ '\
/
* \
10
a b
Par ailleurs, une traduction naïve du code intermédiaire peut générer un code
cible, mais pas toujours efficace. Par exemple, si la machine cible possède une
instruction d'incrémentation (INC), l'instruction à trois adresses a := a + 1, peut
être implantée plus efficacement par la simple instruction INC a, plutôt que par la
séquence qui consiste à charger d'abord la valeur se trouvant à l'adresse mémoire
a dans un registre, ajouter 1 au contenu de ce registre et ranger enfin le résultat à
l'adresse mémoire a ; ce qui nécessite au total trois instructions.
Au cours de toutes ces phases, il est nécessaire de maintenir une table dite des
symboles, qui mémorise les symboles utilisés dans le programme source et les
attributs qui leurs sont associés (type, adresse, valeur, etc.).
Une autre tâche importante que doit réaliser un compilateur est la gestion des
erreurs avec des techniques qui le plus souvent permettent au compilateur de
reprendre le travail d'analyse après la détection d'erreurs, et qui quelquefois
permettent la correction d'erreurs simples.
134 Chapitre 4
Le résultat délivré par l'analyse lexicale consiste en une séquence codée formée
d'unités lexicales associées respectivement aux différents lexèmes qui forment
l'instruction d'affectation à compiler.
La séquence idfi assign idf2 mult idfs plus nbr correspond au résultat escompté
et elle constitue le flot d'entrée de la prochaine phase, à savoir, l'analyse
syntaxique.
y:=a*b+lO
.....-----~ -~
analyseur lexical Table des symboles
Unités lexicales
~
...............
id.f, 1 1
1 Nom Description
assign [:=] 1
2
I_ - - - - - - - - -> y -
id.f, 2 -----------.,-------- --> 3 a -
mult [*]
-------- b -
id.f, 3 ------------ 1
plus [+]
nbr, 10 idfi assign idf2 mult idfs plus nbr
analyseur syntaxique
assign [ :=]
Table des symboles
Nom Descrintion
idfi plus [+]
y variable simple,
mult[*]/ ~ réelle. adresse #1
/ ~ nbr[lO] a variable simple,
réelle, adresse #2
idf2 idfs b variable simple,
réelle, adresse #3
Introduction à la compilation 135
analyseur sémantique
assign [ :=]
~
plusRéel [+]
Tl := idf2 * idfs
idft := Tl + 10.0
-}
générateur de code final
10
pointeur vers la *
table des symboles b
pour la variable y
Pour avoir une idée sur les autres outils - assembleur et relieur-chargeur - qui
font partie de l'environnement du compilateur, on s'appuie sur l'exemple simple
de la séquence de code suivante représentant l'instruction d'affectation
y:= X+ 10:
MOV x, Rl
ADD #10, Rl
MOV Rl, y
Ces programmes (assembleur et relieur-chargeur) peuvent être utilisés au cas
où ce n'est pas le compilateur qui génère lui-même le code cible translatable et/ou
exécutable. Actuellement, plusieurs compilateurs sont autonomes et effectuent
eux-mêmes les tâches d'assemblage, d'édition de lien et de chargement [Aho, 86].
L'assembleur
La forme d'assemblage la plus simple s'effectue en deux passes :
Au cours de la première passe : i) lecture de la séquence de code comme celle
proposée ci-dessus ; ii) collecte et insertion de tous les identificateurs dans une
table des symboles (pas celle du compilateur), avec leurs emplacements
mémoire comme illustrée par le Tableau XIII.
Introduction à la compilation 137
IDENTIFICATEUR ADRESSE
X 0
y 4
Les deux bits suivants 01 indiquent que c'est le registre 1 qui est utilisé
dans les trois instructions. Les deux bits suivants indiquent le mode
d'adressage concernant l'opérande représenté par les huit bits suivants (les
derniers huit bits). Si les deux bits en question sont à OO, cela signifie qu'on a
un adressage ordinaire, c'est-à-dire les huit derniers bits représentent une
adresse mémoire. Lorsque les deux bits sont à 01, cela signifie qu'il s'agit d'un
adressage immédiat, c'est-à-dire que les huit derniers bits constituent
l'opérande ; c'est le cas de la deuxième instruction.
Le caractère étoile * apparaissant attaché à l'opérande de la première
instruction et la troisième instruction est le bit indiquant une translation.
Donc, si le code est chargé à l'adresse X, alors on ajoute X à l'adresse contenue
dans une instruction comportant le caractère *. A titre d'exemple, si l'adresse
X est égale à 00001010, c'est-à-dire 10, on aura respectivement les adresses 10
et 14 pour les identificateurs x et y de la séquence de code de l'instruction
d'affectation y := x + 10.
138 Chapitre 4
Il est courant de regrouper plusieurs phases en une seule passe et que leurs
activités soient coordonnées par l'analyseur syntaxique. Ce denier sollicite
l'analyseur lexical pour qu'il isole le prochain lexème et lui renvoyer l'unité
lexicale demandée ou juste celle rencontrée. L'analyseur syntaxique fait appel au
module de gestion de la table des symboles pour traiter une nouvelle entité
lexicale ; en cas d'erreur, il appelle le module de traitement des erreurs.
Introduction à la compilation 139
La compilation est une traduction des programmes qui permet, entre autres,
de préparer statiquement une partie des traitements, indépendamment des
données.
L'interprétation évite, en quelque sorte, la séparation du temps de traduction
et du temps d'exécution, qui sont simultanés. Au cours de l'interprétation, tout le
processus (la traduction et l'exécution) se déroule en mémoire centrale,
contrairement au processus de compilation qui lui, produit un code cible
équivalent au programme source avant l'exécution. Le code cible peut être du
code machine réel (concret) ou du code machine virtuel (abstrait).
analyse lexicale
unités lexicales --.....1 L
~ analyse syntaxique r- r---~~~ti~~-d~----i
~ ->! la table des !
arbre syntaxique ---1 analyse sémantique rlr_
~. ! symboles !
. /~---~ ~--- ------ ----- ---___ J
b
ar re syntaxique génération de code r--ï;~;ï~~~~t--1
décoré "-. intermédiaire
~.----~~~~~~~ - ~--~:~--~~:~1:1-~~--i
code intermédiaire 1 t" . t"
~- op im1sa ion
d e co d e
/ ............
code intermédiaire ~-------~ ' opération
~ génération de code
optimisé optionnelle
code cible
programme
source
i
t instruction (ou expression) textuelle
i
évaluation
i
résultat
Le cycle d'un interpréteur, quant à lui, se présente comme sur la Figure 67, à
savoir :
la lecture et l'analyse d'une instruction (ou d'une expression) ;
si l'instruction est syntaxiquement correcte, l'exécuter (ou évaluer
l'expression) ;
passer à l'instruction suivante.
Ainsi, contrairement au compilateur, l'interpréteur exécute les instructions ou
évalue les expressions, une à une, au fur et à mesure de leur analyse pour
interprétation
En pratique, il existe une continuité entre interpréteurs et compilateurs. Même
dans les langages compilés, il subsiste souvent une part interprétée (souvent les
formats d'impression comme ceux du langage Fortran, restent interprétés).
Réciproquement, la plupart des interpréteurs utilisent des représentations
intermédiaires (arbres abstraits et même le code octet ou bytecode) et des
traitements (analyses lexicale et syntaxique) analogues à ceux des compilateurs.
Cette technique dite mixte est à mi-chemin entre les interpréteurs et les
compilateurs. Elle combine les avantages des schémas de compilation et
d'interprétation, et améliore, de fait, la portabilité des programmes entre
machines, via un langage intermédiaire standard.
142 Chapitre 4
Compilateur Interpréteur
l'interprétation directe
Le code généré s'exécute
est souvent longue
directement sur la machine
(appel
Efficacité physique. En outre,
de sous-programmes).
ce code peut être
pas de gain sur les
optimisé.
boucles ...
lien direct entre
pas toujours facile de relier instruction et exécution.
Mise-au-point une erreur d'exécution au possibilités étendues
texte source. d'observation et trace
intégrées.
toute modification du texte
source impose de refaire
Cycle de cycle très court
le cycle complet
modification (modifier et ré exécuter)
(compilation,
édition de liens, exécution)
Portabilité limitée assez bonne
On utilise les automates d'états finis qui sont d'excellents modèles pour la
reconnaissance de ces lexèmes.
Par exemple :
Les constantes entières peuvent être décrites par :
• l'expression régulière c+ avec ce {O, 1, ... 9},
• la grammaire S ~ c / cS
• l'automate fini dont le diagramme de transition est celui de la Figure 68.
c,l Ai
~y
c, 1
Dérivation
On suppose donnée une grammaire simple qui engendre un sous-ensemble des
expressions arithmétiques. La liste de ses règles est donnée comme suit :
E~ E +T (l) 1 E - T (2) 1 T (3 ) avec c E {0,1...9}
T~c (4)
Si on dérive le mot 11 6 - 1 + 7 11
par la gauche, on obtient la dérivation canonique gauche 1t1 comme suit :
E ~(l) E + T ~( 2 ) E - T + T ~( 3 ) T - T + T ~( 4 ) 6 - T + T ~( 4 ) 6 - 1 +T
~( 4 ) 6 -1 + 7.
par la droite, on obtient la dérivation canonique droite 1tr comme suit :
E ~(l) E + T ~( 4 ) E + 7 ~( 2 ) E - T + 7 ~( 4 ) E - 1 + T ~( 3 ) T - 1 + T ~( 4 )
6 - 1 + 7.
Donc, la trace de la dérivation gauche est 1t1 = 1 2 3 4 4 4 ; celle de la
dérivation droite c'est 1tr = 1 4 2 4 3 4.
On rappelle que la dérivation canonique gauche représente la trace de l'analyse
descendante, la dérivation canonique droite représente l'inverse de la trace de
l'analyse droite.
L'arbre de dérivation (ou arbre syntaxique) d'un mot peut être obtenu selon la
dérivation gauche ou la dérivation droite ; il suffit de remarquer que les numéros
de règles de 1t1 du mot 11 6 - 1 + 7 11 sont exactement les mêmes que ceux de 1tr.
146 Chapitre 4
E
T
6 1 + 7
~+~
~-~ 7
6 1
(1) s (2) ~
(Îî
s
6 1 + 7 6 1 + 7
dire égale à -2. Dans les deux cas, c'est le nœud le plus interne qui est évaluée en
premier. La grammaire équivalente non ambigüe E ~ E + T (l) 1 E - T (2) 1 T (3) ;
T ~ c (4 l, donnée plus haut, empêchait cette double interprétation. Ainsi, pour
éviter tout conflit, il faut travailler avec une grammaire non ambiguë.
On peut, à la limite, dans certains cas, utiliser une grammaire ambigüe, si elle
présente certains avantages, mais à condition de lui imposer certaines règles. Par
exemple, dans le cas d'une expression, la règle consiste à fixer les priorités
d'exécution des opérations. En effet, si l'on reconsidère l'expression 6 - 1 + 7,
l'ambiguïté sera levée en faveur de l'arbre (1) de la Figure 73 en vertu de
l'associativité à gauche des opérateurs ( + et -) qui attribue la priorité la plus
élevée à l'opérateur qui est situé le plus à gauche.
Associativité des opérateurs
Dans l'expression 6 - 1 + 7 l'opérateur 11 - 11 est associatif à gauche, c'est pourquoi
6 - 1 + 7 est équivalente à (6 - 1) + 7.
Dans la plupart des langages de programmation les quatre opérateurs
arithmétiques (+, -, * et /), sont associatifs à gauche.
L'exponentiation Î est associative à droite. Par exemple, AÎBÎC est traitée
comme A Î(BÎC), c'est-à-dire que c'est d'abord B qui est élevé à la puissance C,
qui donne implicitement un résultat avec lequel sera élevé à la puissance le A.
De même l'affectation est associative à droite. Par exemple, dans le langage C, la
double affectation x = y = z est traité comme x = (y = z).
Priorité des opérateurs
L'expression 6 + 1 / 7 peut avoir deux interprétations possibles (6 + 1) / 7 et
6 + (1 / 7), et l'associativité de + et / ne peut, à elle seule, résoudre ce conflit.
Pour cela, il va falloir définir la priorité relative des opérateurs. Dans
l'arithmétique usuelle, la convention fait que les opérateurs multiplicatifs (* et /)
ont une priorité plus élevée que celle des opérateurs additifs(+ et-). Dans ce cas,
l'expression 6 + 1 / 7 est équivalente à 6 + (1 / 7). Donc, si deux opérateurs ont
des priorités différentes, c'est celui qui a la priorité la plus élevée qui s'exécutera
le premier quelle que soit sa position. En effet, 2 + 5 * 5 est équivalente à
2 + (5 * 5), et 2 * 5 + 5 est équivalente à (2 * 5) + 5. Mais, si deux opérateurs
ont la même priorité, c'est l'associativité qui tranche en faveur de l'opérateur le
plus à gauche. Par exemple, 2 * 5 / 5 est équivalente à (2 * 5) / 5, tout comme
6 - 5 + 7 est équivalente à (6 - 5) + 7. Enfin, il faut noter que les opérations à
l'intérieur des parenthèses sont toujours plus prioritaires. Par exemple, 6 -
(5 + 7) est égale à 6 - 12 = - 6, et (6 * (3 + 1)) - 5 est égale à (6 * 4) - 5 = 24 -
5 = 19.
Notation préfixée
On reconduit le même principe que la notation post-fixée, sauf que pour la
notation préfixée, l'opérateur doit précéder les opérandes et non l'inverse comme
avec la notation post-fixée. Par exemple, l'expression a * b + c, possède pour
expression préfixée l'expression + * a b c. En effet, en procédant de la même
façon que pour l'exemple de la notation post-fixée, on a l'expression a* b + c, qui
donne d'abord+ (a*b)' c' qui est égale à+* a' b' c, laquelle est finalement égale
à + * a b c, qui correspond au résultat attendu.
Notation et spécification
Comme préconisé, une traduction définit une correspondance entre un texte
d'entrée et un texte de sortie. On spécifie le texte de sortie comme suit :
Après avoir construit l'arbre de dérivation pour un texte d'entrée, on considère un
nœud dénoté par le symbole A de la grammaire. On note alors "A.t" la valeur de
l'attribut "t" de "A" à ce nœud. Un arbre syntaxique donnant les valeurs des
attributs à chaque nœud est un arbre annoté et décoré.
Ainsi, pour construire, par exemple, une définition dirigée par la syntaxe
traduisant une expression arithmétique en sa notation post-fixée, on associe à
chaque symbole non-terminal A un attribut "t" dont la valeur notée "A.t" est
l'expression post-fixée du membre droit de la production engendrée par A. En
d'autres termes, si A ~ a est une production, alors A.t = a' où a' est la
représentation post-fixée de a.
E
T
6 1 +
Figure 77: Arbre syntaxique de l'expression 11 6 - 1 + 7"
E.t = 6 1- 7 +
~T.t = 7
E.t = 6 1-
6 1 + 7
Figure 78 : Arbre syntaxique annoté et décoré de l'expression 11 6 - 1 + 7"
~ ~\~
•.,.. 1 -
E.t =6 T.t =6
~ \, T.t =1
'1' ~ 7 T-+ 1
T
*
Figure 81 : Arbre syntaxique d'un schéma de traduction
La différence entre les deux modèles de traduction n'est pas que dans le
formalisme, mais aussi dans la manière de mener la traduction. En effet, dans le
cas de la définition dirigée par la syntaxe, le résultat est attaché à la racine de
l'arbre syntaxique, alors que dans le cas du schéma de traduction dirigée par la
154 Chapitre 4
syntaxe, le résultat est émis en sortie de manière incrémentale. Les deux modèles
sont illustrés sur l'expression a * (a+ a) respectivement par les Figures 82 et 83.
On peut associer à la définition dirigée par la syntaxe du Tableau XVII, le
schéma de traduction par la syntaxe du Tableau XVIII.
E.t =a a a+*
T.t =a a a+*
/ F.t =a a+
T.t =a 1
/ E.t =a a+
............-;~
F.t =a
T.t =a
/
F.t =a
F.t =a
/
a * a + a
E
1
~ ~ J {imprimer('*')}
T * F
F
/ (
/\~
E __ )
/ \ ----1~-------
E + T {imprimer ('+')}
a {imprimer ('a')}
T/ \
/ F
/ F ',,,
a {imprimer ('a')}
a {imprimer ('a')}
1 Introduction
Les tâches principales de l'analyse lexicale sont la lecture du flot d'entrée
(programme source) et la production, en sortie, d'une suite d'unités lexicales
(nommées aussi tokens) qui sera utilisée par l'analyse syntaxique. Le but étant de
réduire la longueur du programme source afin de gagner du temps au cours des
prochaines phases.
Outre la reconnaissance des entités lexicales (identificateur, constante, etc.),
l'analyse lexicale peut également assurer d'autres tâches telles que :
- le rangement de certaines constructions, comme les identificateurs ou les
constantes, dans les tables appropriées destinées à cet effet ;
- l'élimination des espaces blancs, des commentaires, des caractères de tabulation
ou de fin de ligne, et de tout autre symbole superflu, dans un souci d'optimisation
des traitements au cours des prochaines phases ;
- la détection des erreurs d'ordre lexical et leur signalement par des messages
explicites.
Un problème initial posé par l'analyse lexicale consiste en le choix des modèles
associés aux lexèmes. On rappelle qu'un lexème ou entité lexicale est un mot du
langage source (mot-clé, constante, opérateur, etc.). Le choix de certains modèles
est un problème peu formel. En effet, par exemple, pour les nombres complexes en
Fortran, décrits par le modèle (<réel>, <réel>), il y a deux stratégies
envisageables :
l'une, permet de considérer <réel> comme un modèle de lexème pour les
nombres réels, et dans ce cas précis, on laisse l'analyse syntaxique s'occuper de
la reconnaissance d'une constante représentant un nombre complexe suivant le
modèle (<réel>, <réel>).
l'autre stratégie, suggère que (<réel>, <réel>) est un modèle et, par
conséquent, c'est à l'analyseur lexical de traiter ce modèle en tant que tel, et
Analyse lexicale 157
<ldf>;
<*>;
<nbr>.
Les analyseurs lexicaux reposent tous sur le même principe et travaillent pour
le même but, à savoir, la catégorisation des lexèmes et leur remplacement, chacun,
par le token approprié. L'analyse lexicale ne nécessite que des algorithmes simples.
Une unité lexicale (token) sera généralement représentée par un couple, noté
(<type>, <donnée>) ou (<type>, <pointeur vers la donnée>). Parfois (cas des
constantes ou des opérateurs), une unité lexicale est représentée uniquement par
une seule composante du couple, à savoir, <type>.
Programme ~ .+ Prg~l
source ...,,,,.- ..
_,,'
,.-
\
~
Prg 01
On peut tout aussi adopter une autre démarche où l'analyseur lexical est un
sous-programme ou une coroutine de l'analyseur syntaxique. Le schéma de la
Figure 85 illustre ce type de configuration.
unité lexicale
Programme
analyseur
source .+ lexical
demande de la prochaine
unité lexicale
Pour des raisons techniques évidentes, c'est cette dernière possibilité qui est
souvent choisie en pratique. En effet, dans cette option, il n'y a pas de fichiers
intermédiaires mis en jeu, ni de temps morts entre deux phases de compilation.
Mais, là également, on peut parler de deux modes d'utilisation d'un analyseur
lexical.
Mode direct : l'analyseur lexical peut être appelé pour reconnaitre n'importe
quel lexème et renvoyer son type (unité lexicale), en réponse, à l'analyse
syntaxique.
Mode indirect : l'analyseur lexical est appelé pour reconnaitre un lexème d'un
type spécifié, et renvoyer «oui» ou «non», en réponse, à l'analyseur
syntaxique.
On reconsidère l'exemple de l'instruction DO 10 1 = 1, 15 de la section 1 de ce
chapitre avec le pointeur déterminant la position du 1er caractère du lexème à
reconnaitre (l'extrémité gauche du lexème). En mode indirect, l'analyseur lexical
répondrait par « oui » s'il est appelé pour identifier le token DO (mot-clé réservé)
ou l'identificateur DOlOI. Dans le premier cas, le pointeur se déplace de deux
positions à droite. Dans le deuxième cas, le pointeur se déplace de cinq positions à
droite. En mode direct, l'analyseur lexical examine l'instruction DO 10 1 = 1, 15
jusqu'à rencontrer la virgule ",", auquel cas, il conclut que le lexème reconnu
correspond bien au token du mot-clé DO. Le pointeur se déplace ainsi de deux
positions à droite, quoique plusieurs autres symboles aient été déjà scannés au
cours du processus de lecture. La gestion des déplacements du pointeur de lecture
des caractères du flot d'entrée est illustrée dans la section 5 de ce chapitre.
Remarque 2.1
En général, on décrira des algorithmes d'analyse syntaxique en supposant que
l'analyse lexicale est directe. Les algorithmes avec « retour arrière » (non
déterministes) peuvent être utilisés avec l'analyse lexicale indirecte.
Une unité lexicale (token) est généralement un couple composé d'un code et
d'une valeur d'attribut. La nécessité d'avoir un couple (<type>, <donnée>)
ou (<type>, <pointeur vers la donnée>), représenté par le couple (code,
valeur d'attribut) est motivée par la distinction de certaines entités, comme
les identificateurs ou les constantes. Dans ce cas, le premier composant du
couple, en l'occurrence, code, doit impérativement être accompagné d'une
valeur d'attribut pour que l'unité lexicale appropriée soit distincte. Dans le
Analyse lexicale 161
Tableau XIX, sont répertoriés les lexèmes et les unités lexicales correspondant
à l'instruction d'affectation X :=Y Î 10.
Dans certains couples, il n'est pas nécessaire d'avoir une valeur d'attribut,
comme c'est le cas de l'affectation ou de !'exponentiation. En revanche, comme
prévu, <idf> est le code qui peut concerner plusieurs identificateurs distincts
(ici, X et Y). Pour les distinguer, on ajoute la valeur d'attribut qui est l'entrée
dans la table des symboles de chaque identificateur {X et Y). De même,
<nbr>, a pour valeur d'attribut, la valeur de la constante (ici 10), mais le
compilateur peut aussi ranger le lexème 10 dans la table des symboles et
fournir comme valeur d'attribut, l'entrée dans la table des symboles.
Un modèle est une règle qui décrit l'ensemble des lexèmes pouvant représenter
une unité lexicale particulière dans un programme source. Un modèle peut être
formel (expression régulière, automate fini, grammaire régulière, etc.), ou
informel comme explicité par les entités du Tableau XX.
unité
lexèmes description informelle des modèles
lexicale
<const> const constante
<if> if condition if
<oprel> < <= < <>
< <= < <> > >=
> >=
mots formés de lettres ou de chiffres
<idf> X, pi, Y2 commençant par une lettre et de longueur
inférieure à 6
<nbr> 3.14, 0.9E+3 constantes réelles
<littéral> 11 xyyzt 11 chaine de caractères entre 11 et 11 sauf 11
4 Classes de lexèmes
On peut répartir les mots d'un langage en groupes ou classes. Les principales
sortes d'unités lexicales que l'on rencontre dans les langages de programmation
courants sont :
162 Chapitre 5
les mots-clés : if, then, else, while, do, repeat, for, etc.
les identificateurs : i, Xl, Y2, vitesse, etc.
les constantes littérales : 10.5, -54, 43.3E+2, -5, aplus, etc.
les +,
caractères spéciaux simples : =, -, *, etc.
les caractères spéciaux doubles : <=, ++, :=, etc.
begin et end sont des mots-clés réservés jouant le rôle de délimiteurs de blocs.
Ils marquent également le début et la fin d'une procédure, de fonction et/ou de
programme;
etc.
166 Chapitre 5
En somme, tous les mots-clés utilisés comme une constante booléenne (true ou
false) ou comme opérateurs logiques (not, and et or) ou encore comme
opérateurs arithmétiques (round, mod et div) etc., doivent être traités d'abord
en tant que tels au niveau de l'analyse lexicale. Ensuite, il va falloir aussi garder à
l'esprit que ces mots-clés nécessitent un traitement spécial. Par exemple, on doit
connaitre la valeur effective (1 ou 0) de true ou false, et lui faire jouer son rôle
de constante prédéfinie, le moment venu (au cours de la phase de traduction). Il
en est de même, en ce qui concerne les autres cas, à savoir (not, and et or) et
(round, mod et div), qui représentent respectivement des opérateurs logiques et
des opérateurs arithmétiques, etc. Cette remarque concerne tout mot-clé ; qu'il
représente une fonction, un opérateur, un délimiteur, une valeur prédéfinie, etc.
Cette codification apparait donc comme une nécessité pour préparer et alléger
les prochaines phases du compilateur. En ce qui concerne les mots-clés, on n'a pas
besoin de spécifier le deuxième champ du couple <code, valeur d'attribut>, le
premier composant suffit pour identifier tout mot-clé d'un langage considéré,
puisque par définition chaque mot-clé remplit une fonction bien définie qui lui est
propre. Ainsi donc, un code associé à un mot-clé suffit à le distinguer de tous les
autres. Il en est de même pour les opérateurs ou les séparateurs ; leurs unités
lexicales sont renvoyées à l'analyse syntaxique sans les attributs. Ces derniers sont
utiles au cours de la traduction. Par exemple, la séquence d'entrée : begin x :=
64 + y end, sera transformée en la suite de couples comme suit :
<code begin, >
<idf, pointeur à x>
<Assign, >
<nbr, valeur entière 64>
<+, >
<idf, pointeur à y>
<code end, >
On note que dans certains couples, aucune valeur d'attribut n'est nécessaire ;
le premier composant suffit pour identifier le lexème reconnu. Ici, on a associé à
l'unité lexicale du lexème 11 64 11 un attribut à valeur entière. Le compilateur peut
Analyse lexicale 167
ranger le lexème dans la table des symboles et faire en sorte que l'attribut de
l'unité lexicale associée, soit un pointeur vers la table des symboles.
5 Technique de bufferisation
Quelle que soit la stratégie adoptée, l'analyse lexicale est toujours basée sur des
expressions régulières que l'on transforme généralement en automates d'états finis.
Il existe trois approches pour construire un analyseur lexical. Par ordre de
difficulté croissante, on a :
utilisation d'un générateur (constructeur automatique d'analyseurs lexicaux)
comme Lex ou Flex;
écriture manuelle de l'analyseur lexical, en s'appuyant sur un langage de haut
niveau comme C, Pascal, etc. ;
écriture manuelle de l'analyseur lexical en utilisant un langage d'assemblage.
L'analyseur lexical le plus efficace est, sans aucun doute, produit par la
dernière approche parmi les trois précédentes. En effet, un code écrit à la main
avec un langage d'assemblage est évidemment beaucoup plus dense (optimal),
donc plus efficace que celui produit par un compilateur.
L'analyse lexicale est la seule phase du compilateur qui lit le programme
source, caractère par caractère, et elle prend généralement un temps considérable,
même si les autres phases sont conceptuellement plus complexes [Aho, 86]. Mais,
pour gagner en efficacité, il est nécessaire d'utiliser des techniques élaborées qui
faciliteront la lecture et l'analyse des flots d'entrée. Ainsi, au lieu de lire le
programme source caractère par caractère, on utilise un buffer (tampon) de la
manière illustrée par la Figure 86
1 · · · · · · · D · · - · · B ·*·*·2 # - : 4 : * : A :* :C# : : : : : : : :#
début-do-lex~,- .. . . J î
lookahead
la rapidité dans la lecture du flot d'entrée. En effet, une moitié du buffer est
rangée en mémoire centrale ;
les pointeurs début-de-lexème et lookahead permettent d'isoler chaque lexème
apparaissant dans le buffer.
La procédure de gestion des tampons, au cours de la lecture des caractères
d'entrée et du traitement des unités lexicales correspondantes, se résume dans les
points suivants :
le lexème courant est situé entre début-de-lexème et lookahead ;
initialement, les deux pointeurs sont positionnés sur le 1er caractère du
prochain lexème à reconnaitre ;
le lookahead avance caractère par caractère jusqu'à identifier un modèle ;
le pointeur lookahead avance sur le caractère à droite qui suit le lexème
correspondant au modèle trouvé ;
après le traitement de l'entité lexicale correspondante, les deux pointeurs
(lookahead et début-de-lexème) sont positionnés sur le caractère qui suit
immédiatement le lexème reconnu ;
les blancs et les commentaires sont traités comme des modèles qui ne renvoient
aucune unité lexicale ;
si le pointeur lookahead est sur le point de dépasser la fin de la moitié gauche
(marquée par eof), du tampon, la moitié droite est remplie avec N nouveaux
caractères d'entrée ;
si lookahead est sur le point de dépasser la taille du tampon, il faut charger la
moitié gauche avec N nouveaux caractères d'entrée et réajuster le pointeur
lookahead circulairement au début du buffer.
si lookahead est sur la fin du texte d'entrée (marqué par le caractère eof),
l'analyse est terminée.
Par exemple, si on revient sur le cas des deux instructions du Fortran
présentées plus haut :
DO 10 1=1.15
DO 10 1=1,15
on peut voir comment il est possible de les traiter en appliquent la technique de
mémorisation introduite ci-dessus. Mais, avant cela, il va falloir d'abord les
débarrasser des espaces blancs superflus. Sous leur nouvelle forme, les deux
instructions deviennent :
DOlO 1=1.15
DOlOI = 1,15
Initialement, les deux pointeurs début-de-lexème = 1 et lookahead = 1. Donc,
pour reconnaitre le mot-clé DO, le lookahead doit avancer jusqu'à rencontrer la
virgule « , » ; mais pour reconnaitre l'identificateur DOlOI, le lookahead doit
avancer jusqu'à rencontrer un point « . ».
Si c'est la virgule « , » qui est rencontrée alors l'entité correspond au mot-clé
DO ; donc le pointeur lookahead doit avancer de 2 positions (longueur du lexème
DO = 2). C'est à ce moment que commence effectivement l'analyse du lexème
DO. En effet, si un D puis un 0 sont effectivement rencontrés, alors le mot-clé
Analyse lexicale 169
DO est reconnu. Après avoir traité l'entité lexicale correspondant à DO, on met le
pointeur début-de-lexème à sa nouvelle position, à savoir, début-de-lexème =
lookahead.
Dans le cas de l'identificateur DOlOI, le lookahead avance de 5 positions
(longueur du lexème DOlOI = 5), et après le traitement de l'entité lexicale
correspondant à DOlOI, le pointeur début-de-lexème est mis à sa nouvelle
position, c'est-à-dire début-de-lexème = lookahead.
6 Modèles de spécification
Pour rappel, un modèle est une règle qui décrit l'ensemble des lexèmes (entités
lexicales) pouvant représenter une unité lexicale dans un programme source. Pour
décrire avec précision les modèles d'unités lexicales comme, par exemple, les
identificateurs ou les constantes numériques, on utilise les expressions régulières
dont les concepts de base ont été étudiés au chapitre 2. En effet, les expressions
régulières restent la notation la plus largement utilisée pour spécifier des modèles.
On décrit généralement les identificateurs par l'expression régulière
lettre (lettre Et> digit)*, que l'on note également lettre (lettre 1 digit)*.
Autrement dit, le symbole « 1 » représente le OU qui est habituellement utilisé
dans les règles de production d'une grammaire. L'opérateur puissance *, quant à
lui, signifie 0 ou plusieurs instances du terme qu'il suit. Enfin, la juxtaposition du
terme lettre avec la sous-expression (lettre 1 digit)* correspond à la
concaténation.
Un parenthésage superflu dans des expressions régulières peut être évité si l'on
adopte les conventions suivantes :
l'opérateur unaire « * » a la plus grande priorité et est associatif à gauche ;
la concaténation a la deuxième plus grande priorité et est associatif à gauche ;
l'opérateur binaire « 1 » a la plus petite priorité et est associatif à gauche.
Ainsi, compte tenu de ces conventions, l'expression régulière (a) 1 ((b)*(c)) est
équivalente à l'expression régulière a 1 b *c.
Pour des commodités de notation, on peut adapter le formalisme des règles de
production pour définir des expressions régulières. Cette nouvelle façon de noter
les expressions régulière se nomme désormais définitions régulières.
Un autre exemple de définition régulière, celui des nombres non signés, par
exemple, en Pascal, consiste en l'ensemble des règles suivantes :
digit ~ 0 1 1 1 ... 1 9
digits ~ digit digit*
fraction ~ • digits 1 e
exposant ~ E (+ 1 - 1 e) digits 1 e
nombre~ digits fraction exposant
Si l'on reprend l'exemple précédent des nombres réels non signés en Pascal, on
aura la définition régulière représentée par les règles suivantes :
digit ~ 0 1 1 1 ... 1 9
digits~ digit+
fraction ~ (• digits) ?
exposant ~ (E (+ 1 - ) ? digits) ?
nombre~ digits fraction exposant
On peut également définir les constantes réelles usuelles avec ou sans signe. Si
une constante est < 1, elle peut commencer même par « • ». Il y a plusieurs
manières d'écrire la définition régulière correspondante, l'essentiel est de respecter
l'ordre d'apparition des règles de production correspondantes. On aura ainsi :
digit ~ 0 1 1 1... 1 9
Analyse lexicale 171
signe ~ (+ 1- ) ?
entier~ signe digit +
décimal ~ signe (digit * • digit + 1 digit + • digit *)
constante ~ entier 1 entier E entier 1 décimal 1 décimal E entier
Classes de caractères
[a b c] signifie : a 1 b 1 c
[a - z] signifie : a 1 b 1 c ... 1 z
[A - Z a - z][A - Z a - z 0 - 9] * dénote l'ensemble des identificateurs.
lettre~ A 1 B ... 1 Z
digit ~ 0 1 1 1... 1 9
idf ~ lettre (lettre 1 digit) *5
Mais, si on ne souhaite pas autoriser les mots-clés d'être utilisés comme des
identificateurs, on peut réviser cette définition en y excluant les mots-clés. On
obtient finalement la définition régulière modifiée suivante :
lettre~ A 1 B ... 1 Z
digit~ 0 1 1 1... 1 9
idf ~ (lettre (lettre 1 digit) *5 ) - (DO 1 IF I · .. )
172 Chapitre 5
Remarque 7.1
L'élimination des informations inutiles (espaces blancs, commentaires, etc.),
peut être réalisée par un préprocesseur avant de démarrer l'opération de
« scanning » du texte source. Elle peut également avoir lieu parallèlement à
l'opération de « scanning ».
Certaines erreurs qui ne sont pas d'ordre lexical (dépassement de capacité pour
les valeurs des constantes, etc.), peuvent aussi être détectées au niveau de
l'analyse lexicale. En effet, les valeurs des constantes numériques sont
généralement calculées parallèlement aux tâches de lecture du flot d'entrée et
d'analyse des entités lexicales ; et c'est au cours de l'opération de calcul de la
valeur d'une constante qu'une erreur de dépassement de capacité peut être
détectée et signalée par l'analyseur lexical.
Les outils les plus couramment utilisés pour analyser les flots d'entrée en vue
d'isoler des lexèmes pour en faire des unités lexicales, sont les automates d'états
finis. Il existe plusieurs approches permettant de simuler le comportement d'un
automate d'états finis, mais on s'intéresse ici à deux techniques très prisées :
Simulation du fonctionnement de l'automate directement à l'aide de son
diagramme de transition ;
Avant de décrire ces deux techniques, il convient de revenir d'abord sur les
deux modes d'utilisation de l'analyse lexicale, évoqués en section 2 de ce chapitre,
à savoir, l'analyse lexicale indirecte et l'analyse lexicale directe:
Analyse lexicale 173
D, F, I, 0
(c) IF
* D. F, 1. 0
Figure 88 : Automate résultant pour l'analyse lexicale
Figure 89 : Automate fini pour l'analyse lexicale des mots-clés for et while
176 Chapitre 5
eLD *
L
<idf, adr>
eD
*
D
<const, val>
=>
LD = {lettres, digits} = [a - z 0 - 9]
D = {digit} = (0 - 9]
L = {lettres} = [a - z]
<idf, adr> : idf est le code d'un identificateur et adr son entrée dans la table
des symboles.
<const, val> : const est le code d'une constante entière et val sa valeur.
Tous les états finals où il faut retirer le dernier caractère lu (qui ne fait pas
partie du lexème reconnu), sont marqués par le symbole aster *. Le dernier
caractère retiré peut être, soit un espace blanc, soit le caractère de début d'un
nouveau lexème auquel cas, il y a tout intérêt à ne pas le perdre ; il peut
également être un symbole spécial indiquant le début d'un commentaire, etc.
Aussi, ces états finals, comme on peut le remarquer, sont des états finals simples
ou d'acceptation, c'est-à-dire des états finals d'où ne part aucun arc vers un autre
état. Théoriquement, comme étudié au chapitre 2, sur les langages réguliers, les
Analyse lexicale 177
17 : readcar (c) ;
cas de
"+" : traiter l'opérateur +
"-" : traiter l'opérateur -
peut se faire très rapidement ; cette approche est donc avantageuse quand la
chaine x est très longue. Cependant, le volume mémoire occupé par la table de
transition (qui est un tableau à deux dimensions), peut être énorme ; plusieurs
centaines d'états multipliés par 128 caractères si la table des symboles, n'est pas
initialisée par les mots-clés. Une solution évidente qui vient à l'esprit serait de
transformer la table de transition en une liste chainée pour ranger les transitions
sortant de chaque état, mais ça sera évidemment au détriment de la rapidité de
l'analyseur. Une structure plus subtile qui allie la compacité de la structure de
liste et la rapidité d'accès à la table de transition standard (tableau à deux
dimensions), consiste en quatre tableaux, comme décrits dans la Figure 93. Ces
tableaux nommés Default, Base, Next et Check sont indexés par les numéros
d'états [Aho, 86].
déterministe
non déterministe
1
~
s q -- r t
Pour déterminer l'état r vers lequel aura lieu la transition sur le caractère a, à
partir de l'état s, il faut accéder d'abord aux tableaux Next et Check par l'index
Base[s] + a. Le caractère a est traité comme un entier par conversion. Dans ce
cas, on vérifie si Check (Base[s] + a) = s, auquel cas, on déduit que Next
(Base[s] + a) représente effectivement l'état suivant noté r vers lequel doit
transiter l'automate sur le caractère a. En cas d'échec de la tentative, on
reconsidère le processus en utilisant Default [s] à la place de l'état s. La procédure
qui renvoie l'état suivant à partir de l'état s, et d'une transition sur le caractère a,
est donc la suivante :
function suivant (s, a) ;
si Check (Base [s] + a) = s alors
suivant f- Next (Base [s] + a)
Analyse lexicale 181
L'état 2 a une transition surf (5) qui est différente de la transition sur l'état 1.
Cette entrée est stockée dans Next [36]. Par conséquent, la valeur de Base [2]
est positionnée à la case 36 - 5 = 31 du tableau Next.
Pour trouver l'état suivant de l'état 2 sur le symbole a, on utilise la fonction
suivant (2, a) qui contrôle d'abord si Check [Base [2] + O] = 2, c'est-à-dire a-
t-on Check [31 + O] = 2 ? Mais, puisque Check [31 + O] -::t. 2, on prend
Default [2] qui est = 1. Donc, on utilise à nouveau (récursivement) la fonction
de contrôle suivant (1, a) qui montre que Check [Base [1] + O] = Check [O +
O] = 1. Par conséquent, Next [Base [1] + O] = Next [O + O] = 1.
Pour remplir les quatre tableaux, on utilise une méthode heuristique. Une
stratégie qui fonctionne bien en pratique, consiste à trouver, pour un état
donné, la plus petite base, c'est-à-dire à positionner Base à l'indice le plus bas
de sorte à remplir les entrées spéciales (comme celle correspondant à l'état 2
par exemple), sans toutefois provoquer de conflit avec les entrées existantes.
3 3 3 1 1
... 1
, 29 1 1
~
-.---+ 31 1 5 1
1 1
f i
35 1 9 1
36 3 2
37 2 0
D D
§ §
Figure 95 : Aperçu de représentation compressée de la table de transition
de l'automate fini de la Figure 94
Analyse lexicale 183
E LD *
<begin>
E LD *
<end>
184 Chapitre 5
D *
e:D
=>
<const, val>
LD
L *
e: LD
=>
<idf, adr>
case start of
0: start := 7; return := 7;
7 : start := 12 ; return := 12 ;
12 : start := 16 ; return := 16 ;
other : erreur
end
end;
function nexttoken ;
begin
state := 0 ; start := 0 ;
while true do
case state of
0: getnonblank; dl := la;
if c = 'b' then state := 1 else state := echec;
5: c := nextchar;
if letter ( c) or digit ( c) then state := echec else state := 6;
la:= la - 1; return ( <begin, >)
16: c := nextchar;
if letter ( c) then state := 17 else state := echec;
17: c := nextechar;
if letter ( c) or digit ( c) then state := 17 else state := 18;
18: la := la - 1; return ( <ident, adr>)
end /* case */
end /* nexttoken */
Remarque 7.2
L'étape de reconnaissance des entités lexicales étant presque maitrisée grâce aux
différents outils comme les automates et leurs modèles de représentation,
particulièrement, les diagrammes ou les tables de transition associées. Mais, au
préalable un problème crucial posé par l'analyse lexicale consiste en le choix des
modèles associés aux lexèmes. On rappelle qu'un lexème est un mot du langage
source (mot-clé, constante, opérateur, etc.).
D D D
D
'E' D
D D
D
D
D <l: D u{'E',•}
*
=>
Il: D
D 'E' D
Figure 98 : Diagrammes de transition pour les nombres réels et entiers
188 Chapitre 5
Sur la Figure 98, les états i, r et 8 sont des états finals d'acceptation pour les
entiers, les réels sans exposant et les réels avec exposant, respectivement. Il faut
donc remodeler le simulateur pour l'adapter au nouveau diagramme de transition
combiné.
De l'état 2 à l'état final d'acceptation i on transite sur I qui est l'ensemble
dont les éléments n'appartiennent pas à [O - 9] u {'E', •}. De même, de l'état 4 à
l'état final d'acceptation r, la transition a lieu sur les éléments de R qui est
l'ensemble complémentaire de [O - 9] u {'E'}. Enfin, 8 est l'état final
d'acceptation des réels avec exposant ; les éléments qui ~ D représentent
l'ensemble complémentaire de D (tous les symboles sauf les chiffres).
L'autre approche, comme annoncé ci-dessus, peut être illustrée en s'appuyant
sur certains cas d'erreurs. Par exemple, soient les trois expressions régulières
suivantes dont les diagrammes de transition sont décrits dans la Figure 99.
a
abb
a· b+
Ici également on fait en sorte que le lexème, pour une unité lexicale donnée,
soit le plus long possible. Par ailleurs, si plusieurs expressions régulières (modèles)
correspondent à un lexème donné, seul le modèle qui apparait en premier dans la
spécification est retenu pour ce lexème.
symbole d'entrée
état modèle annoncé
a b
0137 247 8 aucun
247 7 58 a
8 - 8 a* b+
7 7 8 aucun
58 - 68 a* b+
68 - 8 abb
Les états de l'automate fini déterministe obtenu sont nommés par des sous-
ensembles d'états de l'automate fini non déterministe de la Figure 99.
Parmi les états 2, 4 et 7, seul l'état 2 est un état final d'acceptation pour
l'expression régulière a dans l'automate de la Figure 99 (a). Ainsi, l'état 247
reconnait le modèle a. Cependant, sachant que le lexème a est un préfixe du
lexème abb, le simulateur de l'automate pourra reconnaitre soit a, soit abb, si le
lexème abb est présenté à l'entrée. Ce conflit est levé en faveur du lexème le plus
long, conformément au principe de la stratégie utilisée.
L'analyse du lexème abb correspond par ailleurs aux deux modèles abb et
a*b+, reconnus aux états 6 et 8 de l'automate fini non déterministe de la
Figure 99. L'état 68 de l'automate déterministe correspondant inclut donc les
deux états d'acceptation 6 et 8. Mais, comme c'est abb qui apparait avant a*b+,
alors on déduit que seul le modèle abb est retenu dans l'état 68.
Sur le lexème aaba, l'automate fini déterministe se comporte de la même façon
que son homologue non déterministe. En effet, il permet d'enregistrer le modèle a,
après avoir reconnu le lexème a, à l'état 247 qui est un état final d'acceptation.
190 Chapitre 5
Ensuite, après les transitions par les états 58 et 68, on enregistre le modèle a*b+
au niveau de l'état 68 qui est un état final d'acceptation. Cependant, au niveau de
ce dernier, il n'y a pas de transition possible sur le caractère a ; on a donc atteint
un état de blocage. On revient donc au dernier modèle enregistré, c'est-à-dire
a*b+.
De même, avec le lexème aba, l'automate déterministe adopte le même
comportement, à savoir, il débute à l'état 0137 et passe à l'état 247 sur le
caractère a, en enregistrant au passage le modèle a. Il progresse ensuite jusqu'à
l'état 58 sur le caractère b, en enregistrant le modèle a*b+, mais sur le dernier
caractère a, il n'y a pas de transition possible ; on a donc atteint un état de
terminaison. L'état 58 est un état final, puisqu'il contient l'état 8 qui est un état
final dans l'automate de la Figure 99. A l'état 58, l'automate conclut que le
modèle a*b+ a été reconnu et choisit comme lexème ab, le préfixe du texte d'entrée
qui a conduit à cet état.
Remarque 7.3
Une série de mesures à prendre au préalable afin d'implémenter un analyseur
lexical efficace pour un langage donné, pourrait se résumer en les points suivants :
1) Spécifier chaque type d'entité lexicale à l'aide d'une expression régulière ;
2) Représenter chaque expression régulière par l'automate d'états finis
équivalent ;
3) Construire l'automate « union » de tous les automates de l'étape 2 ;
4) Rendre déterministe l'automate résultant de l'étape 3 ;
5) Minimiser le nombre d'états de l'automate obtenu à l'étape 4 ;
On reconsidère l'exemple de la Figure 96 et on construit le diagramme de
transition de l'automate "union" de tous les diagrammes correspondants (étape 3).
L'automate déterministe obtenu après l'étape 4 de la remarque 7.3 est celui de la
Figure 100.
b
#
e # g # # # *
e: LD
:::::>
<begin>
e: LD
*
<end>
D
e: D *
<const, val>
LD *
e:LD
<ident, adr>
Pour diminuer encore davantage le nombre d'états, les transitions vers les
états finals d'acceptation (ceux annotés par *), peuvent être supprimées comme
suit:
A partir de l'état 0, parcourir le diagramme de transition sur la chaîne d'entrée
la plus longue possible.
Si le dernier état, sur lequel il n'y a pas de transition sur le caractère d'entrée
suivant, est un état final, alors retourner le token correspondant à l'état final,
sinon renvoyer un message d'erreur.
cette table, décrite en section 7.4 du présent chapitre. Dans le cas contraire, on
peut simuler directement le fonctionnement de l'automate en parcourant son
diagramme de transition.
# # # #
b e #
=>
L - {b, e}
(a) génération
(b) compilation
~ a b c
0 1 ŒJ ŒJ
1 1 ŒJ 0
2 0 0 0
3 0 0 ŒJ
Tableau XXIV- Matrice de transition de l'automate de la Figure 103
Selon le type de générateur, l'automate peut être spécifié sous forme tabulaire
auquel cas, il sera nécessairement accompagné de la fonction pilote (comme par
exemple nexttoken), qui simule son fonctionnement pour reconnaitre les lexèmes.
Il peut également être spécifié sous forme d'un programme opérationnel en
utilisant la technique des diagrammes de transition vue dans les sections
Analyse lexicale 195
précédentes. Il est évident, comme annoncé plus haut, que l'approche basée sur la
table de transition est plus générale, quel que soit l'automate, mais aussi plus
efficace, une fois la table construite. Cependant, la construction de cette table est
une opération longue est délicate. On décrit dans la section 8.2, comment on peut
générer automatiquement ce type de table.
function nexttoken ;
begin
state := 0;
c := nextchar ;
while nextstate [state, c] -:;:. 11 - 11 do
begin
state := nextstate [state, c] ;
c := nextchar
end
if not final (state) then
begin
error ; return
end
else begin
unput ( c) ;
action; return
end
end;
end
end;
1 Son nom est un acronyme récursif qui signifie en anglais GNU's Not UNIX (littéralement, GNU
n'est pas UNIX). Il reprend cependant les concepts et le fonctionnement d'UNIX. Le système GNU
permet l'utilisation de tous les logiciels libres, pas seulement ceux réalisés dans le cadre du projet
GNU.
www.bibliomath.com
Analyse lexicale 197
3}
déclarations de définitions régulières
Deuxième section (règles de traduction)
33
règles de traduction
Troisième section (bloc principal et fonctions auxiliaires)
33
procédures auxiliaires
Programme
compilateur lex.yy.c
source Lex
Lex
lex.l
compilateur
lex.yy.c c a.out
flot suite
a.out d'unités
d'entrée
lexicales
Figure 104 : Création d'un analyseur lexical à l'aide de Lex [Aho, 86]
Une constante littérale est un identificateur qui est déclaré pour représenter
une constante. Une définition régulière en Lex est utilisée comme une macro dans
les actions des règles de traduction. Par exemple, lettre [A - Z a - z] et chiffre
[O - 9] sont des définitions régulières qui dénotent respectivement des lettres et
des chiffres. Une définition régulière permet, en fait, d'associer un nom (comme
chiffre ou lettre) à une expression régulière Lex, et de se référer par la suite (au
niveau définitions subséquentes ou au niveau de la section des règles de
traduction) à ce nom, plutôt qu'à l'expression régulière.
Les règles de traduction sont de la forme :
r {action}
www.bibliomath.com
198 Chapitre 5
La partie déclaration des variables et des constantes littérales, ainsi que les
symboles 3{ et 3} qui l'encadrent peuvent être omis. Quand elle est présente,
cette partie se compose de déclarations qui seront simplement recopiées au début
du fichier généré. On trouve également souvent ici une directive #include qui
produit l'inclusion du fichier « .h » contenant les définitions des codes
conventionnels des unités lexicales (PPQ, EGA, PGQ, etc.).
La troisième section contenant les procédures auxiliaires peut être absente
également (le symbole 33 qui la sépare de la deuxième section peut alors être
omis). Cette section se compose de fonctions C qui seront simplement recopiées à
la fin du fichier généré.
A noter que les symboles 33, 3{ et 3}, quand ils apparaissent, sont écrits au
début de la ligne; aucun blanc ne doit les précéder sur cette dernière.
Avant de donner un exemple complet de programme source pour Lex, il
convient d'abord d'introduire quelques petits exemples élémentaires permettant
de comprendre plus facilement la syntaxe et le format d'un fichier écrit en Lex.
Mais avant cela, il va falloir aussi répertorier, au préalable, sous forme de liste,
dans le Tableau XXV, les constructions d'expressions régulières permises par Lex
( Flex). Dans ce tableau, c représente un caractère, r une expression régulière et s
une chaine.
www.bibliomath.com
Analyse lexicale 199
(r) r (ab 1 c)
\n aller à la ligne -
\t tabulation -
{} faire référence à une définition régulière {idf}
« EOF » fin de fichier (uniquement avec Flex) « EOF »
Tableau XXV- Expressions régulières de Lex (Flex)
r {action}
avec r qui est une expression régulière écrite au début de la ligne en colonne 0
(sans espace blanc qui la précède) ; action, quant à elle, est un fragment de code
source mis entre accolades qui doit commencer sur la même ligne que l'expression
régulière r correspondante. Le fragment d'instructions en question sera recopié tel
quel, au bon endroit, dans la fonction yylex. Cette dernière est une fonction
prédéfinie de Lex ayant pour finalité de lancer Lex.
Ci-après une séquence de règles de type r {action} :
33
while {return TANTQUE ;}
do {return FAIRE;}
{letter}{letterdigit}* {return IDF ;}
{chiffre}+({\.{ chiffre}+) ? {return NBR ;}
Donc, comme évoqué plus haut, une règle du typer {action} signifie qu'après
avoir reconnu une chaine du langage, définie par l'expression r, il faut exécuter
action. Egalement, comme mentionné ci-dessus, le traitement par Lex d'une telle
règle consiste à recopier l'action (action) indiquée à un certain endroit de la
fonction yylex. Quand une chaine du texte source {lexème) est reconnue, la
fonction yylex se termine en rendant comme résultat l'unité lexicale reconnue. Il
faudra appeler de nouveau cette fonction pour que l'analyse du texte source
(programme source Lex) reprenne.
Lex rend les lexèmes accessibles aux fonctions apparaissant dans la troisième
section à travers deux variables yytext et yyleng. La variable yytext correspond à
un pointeur vers le premier caractère du lexème accepté (yytext correspond à
début-de-lexème défini auparavant en section 5 du présent chapitre). La variable
yyleng est un entier donnant la longueur du lexème en qu~stion.
A l'issue de cette esquisse à travers de petits exemples illustratifs, il convient à
présent de donner un exemple plus représentatif qui consiste en un programme
source Lex ayant pour finalité la construction d'une fonction d'analyse pour la
reconnaissance des nombres réels (signés ou non, avec ou sans exposant), des
identificateurs, des opérateurs relationnels, et de certains mots-clés (si, alors,
sinon).
3{
/* définitions des constantes littérales */
PPQ, PPE, EGA, DIF, PGQ, PGE,
SI, ALORS, SINON, IDF, NBR, OPREL
# define PPQ 1
3}
/* définitions régulières */
delim [ \t\n]
bl {delim}+
letttre [A - Za - z]
www.bibliomath.com
Analyse lexicale 201
chiffre [O - 9)
idf {lettre}+( {lettre} 1 {chiffre})*
nombre [+\-]?{chiffre}+(\.{ chiffre}+ )?(E[+\-]?{ chiffre}+) ?
33
{bl} {/* pas d'action ; pas de retour */}
si {return (SI) ;}
alors {return (ALORS) ;}
sinon {return (SINON) ;}
{idf} {yylval = Rangerldf () ; return (IDF) ;}
{nombre} {yylval = RangerNbr () ; return (NBR) ;}
"<" {yylval = PPQ ; return (OPREL) ;}
"<=" {yylval = PPE; return (OPREL) ;}
"=" {yylval = EGA; return (OPREL) ;}
"<>" {yylval = DIF ; return (OPREL) ;}
">" {yylval = PGQ; return (OPREL) ;}
">=" {yylval = PGE; return (OPREL) ;}
33
Rangerldf () {
/*procédure pour ranger dans la table
des symboles le lexème dont le premier caractère
est pointé par yytext et dont la longueur est yyleng
et retourner un pointeur sur son entrée */
}
Ranger Nbr () {
/* procédure similaire pour ranger
un lexème qui correspond à un nombre */
}
www.bibliomath.com
202 Chapitre 5
Pour de plus amples détails sur Lex (Flex), il est conseillé vivement de
consulter le manuel ou le guide d'utilisation Lex (Flex).
La section 8.2 de ce chapitre est consacrée pour décrire une méthode élaborée
utilisée pour construire des reconnaisseurs (automates d'états finis) spécifiés à
partir d'expressions régulières. Cette méthode est adaptée à un compilateur
comme Lex, car elle construit un automate fini déterministe directement à partir
d'expressions régulières, sans passer au préalable par un automate fini non
déterministe que l'on convertit ensuite, au besoin, en automate fini déterministe.
Les notions d'automates d'états finis et d'expressions ont été largement
discutées au chapitre 2, il est donc inutile de revenir sur leur présentation.
Toutefois, de nouvelles techniques élaborées concernant l'utilisation des automates
finis seront introduites dans les tout prochains paragraphes.
www.bibliomath.com
Analyse lexicale 203
1 2
Les nœuds internes sont étiquetés uniquement par des lettres majuscules A, B,
etc. ;
www.bibliomath.com
204 Chapitre 5
abstrait, et sont utilisées pour calculer followpos qui est définie sur l'ensemble des
positions.
Fonction followpos. Définir followpos consiste à calculer followpos ( i) en
répondant à la question : Si on se trouve à la position i de l'arbre, alors quelles
sont les positions à atteindre sur un symbole unique à partir de cette
position?
Autrement dit, cela revient à reconduire le Tableau XXVI avec une légère
modification à la troisième colonne ; cette modification est observée dans le
Tableau XXVII en termes de followpos.
Les règles de calcul des fonctions nullable, firstpos et lastpos sont décrites
dans les tables (a), (b) et (c) du Tableau XXVIII.
Sur la base de ces fonctions, comme annoncé ci-dessus, on définit la fonction
followpos en appliquant scrupuleusement les règles suivantes :
Concaténation c1•c2. Si i est une position qui appartient à lastpos ( c1), alors
tout élément appartenant à firstpos ( c2) est dans followpos ( i).
Etoile c*. Si i est une position dans lastpos ( c), alors chaque position dans
firstpos ( c) est dans followpos ( i).
Itération positive c+. Si i est une position dans lastpos ( c), alors chaque
position dans firstpos ( c) est dans followpos ( i).
nœud n 1
nullable ( n} 1
nœud n 1
firstpos (n} 1
www.bibliomath.com
Analyse lexicale 207
Pour clore ce volet, on regroupe toutes les étapes de calculs des différentes
fonctions, nullable, jirstpos, lastpos et followpos dans un algorithme. On rappelle
que le but final escompté est la construction de l'automate d'états finis de
l'expression régulière (al b).bbal c+ #.
www.bibliomath.com
208 Chapitre 5
procedure dichotomique ( ) ;
begin
if n < taille
then if entrée [n]. identificateur < nom
then begin n := n + 1 ; entrée [n].identificateur :=nom end
else begin
i := 1; j := n;
repeat k := (i + j) div 2;
trouve:= nom= entrée [k].identificateur;
if not trouve then
begin
if nom < entrée [k]. identificateur
then j := k-1
else i := k + 1
end
until ( j < i) or trouve
if not trouve then insertion
else write ('identificateur déjà inséré')
end
else write ('table saturée')
end;
function insertion
begin
for l := n downto i do
entrée [l + !].identificateur:= entrée [q.identificateur;
entrée [i].identificateur := nom;
n := n +1
end;
L'accès par arbre binaire de recherche est, en quelque sorte, une variante de
l'accès dichotomique vu précédemment. En effet, comme envisagé ci-dessus, il
suffit de remplacer la table dichotomique qui est une structure de données
statique, par une structure de liste chainée qui est une structure de données
dynamique. L'accès par arbre binaire ordonné présente la même complexité de
recherche que celle de la table dichotomique, mais une complexité d'insertion
nettement inférieure à celle de la table dichotomique. En bref, l'insertion avec
l'arbre binaire se fait toujours au niveau des feuilles et ne nécessite aucun
déplacement des identificateurs déjà insérés. Par conséquent, l'insertion se fait
toujours en un seul accès une fois la position trouvée.
A chaque nœud de l'arbre est associé un identificateur. En partant de la
racine, la procédure consiste à comparer un identificateur rencontré dans le
programme source à l'identificateur se trouvant à un nœud, s'il est plus grand, on
va à droite, sinon on va à gauche.
Le segment de code suivant donnera l'arbre binaire de la Figure 109.
:= 0 ; s := o. ;
Il
while (n :::.:; 100) do begin read (x); n := n + 1 ; s := s + x end;
Figure 109: Table des symboles basée sur l'accès par arbre binaire ordonné
Les mots notés Jlnnn et JFnnn ne font pas partie du texte source représenté
par le segment de code précédent. Ils ont été créés par le compilateur afin de
pouvoir stocker les constantes numériques qu'ils représentent. Jlnnn pour les
entiers et JFnnn pour les réels. Ici, évidemment on est dans l'option où les
constantes sont stockées dans la table des symboles.
Accès par adressage dispersé ou hachage
Une façon de stocker un identificateur dans une table est de lui affecter un
emplacement calculé par une fonction h. Cette dernière permet de transformer un
identificateur en une valeur de hachage (un index). Dans ce type de rangement,
aussi bien dans le cas de l'insertion que dans le cas de la recherche, un
www.bibliomath.com
212 Chapitre 5
lis a 87
88 1 ma
89 asra
dani 1
98 dani
asra
99
Figure 110 : Table des symboles basée sur l'accès dispersé ; résolution des
collisions par adressage ouvert et sondage linéaire
On peut voir sur la figure, l'identificateur 11 asra 11 qui rentre en collision avec
l'identificateur 11 sara 11 , au niveau de l'entrée 88 ; la collision est résolue par
rehachage linéaire. Le calcul (88 + 1) mod 100 donne 89, qui représente la
nouvelle entrée pour 11 asra 11 dans la table. Il faut noter que l'adressage est ouvert,
c'est-à-dire que les entrées sont internes à la table.
Chainage
On peut parler ici de deux modes de chainage : le chainage interne et le chainage
externe. Le premier a quelques traits de ressemblance avec le rehachage tel qu'il a
été défini précédemment, puisque le chainage est établi à l'intérieur de la table,
c'est-à-dire qu'il se présente comme un adressage ouvert. Quant au second, c'est
un chainage qui provoque un débordement vers une liste externe. Les Figures 111
et 112 illustrent respectivement les deux options de chainage.
Sur la Figure 111, la collision entre les identificateurs 11 sara 11 et 11 asra 11 est
résolue par chainage interne à la table. Ce lien ne donne pas forcément la même
entrée à 11 asra 11 que celle calculée par la technique de rehachage. En effet, l'ordre
d'apparition des entités dans le flot d'entrée ainsi que la stratégie appliquée pour
résoudre les collisions, ont un certain impact sur le calcul de la nouvelle entrée
d'une entité rencontrée lorsque cette dernière rentre en conflit avec une entité
www.bibliomath.com
214 Chapitre 5
déjà présente dans la table. L'exemple de la Figure 113 illustre très clairement ce
cas.
Comme prévu, la Figure 111, illustre le cas de la table des symboles avec le
chainage externe.
lis a
~
87
88 sara
89
dani asra
asra
98
99 §1 1 doni
Figure 111 : Table des symboles basée sur l'accès dispersé ; résolution des
collisions par chainage interne
0
sara
1 lis a
lis a
88 sara asra
dani
89
98 dani
asra
99
Figure 112: Table des symboles basée sur l'accès dispersé; résolution des
collisions par chainage externe
www.bibliomath.com
Analyse lexicale 215
h (lisa) h (asra)
(a) Rehachage
0 1 2 3 4 5 6 7 8 9 10
(b) Chainage
interne
8 9 10
p..
::::-: "'el "'~
Ill
"'Ill Ill ~. "'...,
Ill
3 4
Figure 113: Exemple de table des symboles basée sur l'accès dispersé;
résolution des collisions par rehachage et chainage interne
Les méthodes qui utilisent le chaînage semblent être les meilleures si ce n'est le
champ additionnel, représentant les liens de chainage, qui diminue quelque peu
leurs performances. Sinon, en général, les méthodes de hachage ont de très bonnes
www.bibliomath.com
216 Chapitre 5
performances relativement aux trois autres types d'accès décrits plus haut. En ce
qui concerne le rehachage linéaire, par exemple, même quand la table est remplie
à 903, le nombre de tests est au voisinage de 5. Pour avoir un accès rapide (de
l'ordre de 1), il semble qu'il ne faut pas dépasser un facteur de chargement de la
table de 703.
www.bibliomath.com
Analyse lexicale 217
Nom Attributs
~-~------------------------------------------------------------------·
>-------<------------------------------------------------------------------
div
f---~:------------------------------------------------------------------
mod
count ;------------------------------------------------------------------.
f---~,------------------------------------------------------------------:
' '
~-----<------------------------------------------------------------------·
'
1--<<----'1'-----~.-----------------------------------~
idf, adrl> :
'-+-+-1-~'-----i-----------------------------------·
:
Table des -+---l'+'--i_d_,_f,_a_d_r_2>
_ _,' ___________________________________ ;
noms
1
1
~
V # d # o u n t # #
Cependant, l'enregistrement dans la table des symboles n'est créé que si on est sûr
du rôle syntaxique joué par x.
Pour ce qui est de la portée d'un identificateur, il existe plusieurs solutions
pour sa représentation au niveau de la table des symboles. Ainsi, par
exemple, pour la représentation de la portée des identificateurs du segment
déclaratif suivant :
program fou rien ; niveau 2 niveau 1
var a, x, j: int eger;
proced ure pro cl;
,. bloc 0
bloc 1
var x, y: r eal ·
# r o c 1 # j #
Ainsi, un identificateur n'est visible que si son numéro de bloc et son numéro
de niveau vérifient certaines conditions par rapport au numéro du bloc courant et
au numéro de niveau courant. En effet, une référence à i, y ou k dans le bloc 0
donnera une erreur du genre, « identificateur inconnu ». Par contre, une référence
à a, x ou j, dans n'importe quel bloc est légale.
La solution qui permet la gestion de la table des symboles en pile.
• La recherche d'une entité commence à partir du sommet de la pile ;
Analyse lexicale 219
ayant le même nom (apparaissant dans des blocs séparés ou représentant des
objets différents y compris éventuellement dans un même bloc ou une même
procédure), nécessitent une attention toute particulière. On ne doit pas, par
exemple, organiser l'analyse lexicale comme une phase séparée, au risque de
perdre l'information (type, niveau d'imbrication de procédure ou bloc, etc.),
permettant de distinguer deux identificateurs différents portant le même nom.
nil
a var integer
X var integer
j var integer
pro cl procedure
2 procédure
Proc2
en-tête Pro cl
i var en-tête
Proc3 procedure X var real
y var real
Proc3
en-tête
j var bool
k var bool
trouver aussi qu'un lexème est trop long et signaler cette anomalie ;
etc.
Mais, s'il n'est pas difficile de détecter les erreurs lexicales, il n'est pas du tout
facile de les gérer, afin de permettre au compilateur de continuer à travailler.
Cette continuité est communément nommée reprise ou recouvrement en cas
d'erreur. La stratégie la plus généralement utilisée dans ce contexte est le « mode
panique » qui consiste à :
222 Chapitre 5
X:= Y div Z;
signifie que X n'existe pas en tant que variable pour le compilateur qui adopte la
stratégie « mode panique ». En effet, la variable lX a été ignorée au niveau de la
déclaration ; autrement dit, elle ne sera pas considérée comme une variable
indéfinie au niveau de l'instruction X := Y div Z, c'est ce qu'on appelle une
« erreur secondaire ». Cette dernière sera signalée autant de fois ou plus que X
apparait dans le programme source, et peut même se propager jusqu'au niveau
sémantique. L'utilisateur inexpérimenté est souvent découragé devant un tel
rapport d'erreurs.
Il existe d'autres stratégies de recouvrement qui consistent à corriger le lexème
erroné en proposant un autre à la place tout en signalant l'erreur par un message
d'avertissement. En cas d'erreur sur un lexème, on peut par exemple envisager
de:
prendre son préfixe inférieur à une certaine longueur s'il s'avère trop long par
rapport à la limite supérieure prévue ;
prendre la valeur inférieure au maximum toléré pour une constante ;
échanger, insérer, remplacer, des caractères ;
etc.
La correction d'erreur par transformations (échange, insertion, remplacement)
du lexème erroné est réalisé en se basant sur le calcul du nombre minimum de
transformations à effectuer sur le mot qui pose problème pour en déduire un qui
ne pose plus de problème. On utilise, à cette fin, le calcul de distance minimale
entre les mots. Cette technique de recouvrement (correction) d'erreur est très peu
utilisée en pratique du fait que son implantation revient trop chère ! Par ailleurs,
elle s'est avérée peu efficace, particulièrement, à cause des « erreurs secondaires »
qui se propagent lors des phases ultérieures du compilateur. En effet, si on
l'appliquait sur le lexème lX, on obtiendrait le nouveau lexème X. Mais, si
l'utilisateur prévoyait par exemple la variable AX, cela va engendrer des « erreurs
secondaires», soit lors de l'analyse sémantique, si la variable X, n'a jamais été
déclarée (donc non définie), soit à l'exécution, si X est déclarée, mais sans être de
la même classe ni du même type que AX.
En bref, la stratégie la plus simple reste celle qui repose sur le « mode
panique ». En fait, même avec cette technique, on laisse, en général, le soin à
l'analyseur syntaxique de résoudre le problème.
Chapitre 6
Analyse syntaxique
L'analyse syntaxique constitue l'ossature du compilateur, car c'est elle qui est
chargée de coordonner les différentes tâches nécessaires à l'accomplissement
du processus de compilation dans son intégralité. Elle est considérée comme
la deuxième phase du compilateur, et constitue le principal client de l'analyse
lexicale laquelle est chargée de lui fournir l'unité lexicale appropriée
(réclamée ou juste celle rencontrée) afin de poursuivre le processus d'analyse
d'un programme.
Si on se place dans le contexte d'une compilation en une seule passe,
l'analyse syntaxique :
sollicite l'analyse lexicale qui doit lui renvoyer l'unité lexicale appropriée;
vérifie la conformité du regroupement des unités lexicales en se basant sur
la syntaxe du langage du compilateur ;
appelle les routines de traitements des erreurs en cas d'éventuelles
erreurs;
actual·ise la table des symboles qui est censée être initialisée lors de
l'analyse lexicale;
déclenche les routines de traduction ;
vérifie la sémantique et contrôle les types des différentes constructions ;
etc.
1 Introduction
Le noyau de l'analyse syntaxique qu'on nomme parfois « PARSER » représente
l'analyseur syntaxique proprement dit. Ce noyau est un programme dont la
spécification est une grammaire à contexte libre en général. En appliquant une
stratégie fixée au préalable, le « P ARSER » construit l'arbre syntaxique du
programme source présenté à l'entrée du compilateur.
Il existe deux stratégies principales de fonctionnement pour les analyseurs
syntaxiques : la stratégie descendante (TOP-DOWN) et la stratégie ascendante
(BOTTOM-UP), mais il est possible d'imaginer plusieurs stratégies mixtes.
Avec la stratégie descendante, la construction de l'arbre commence à partir de
la racine (Axiome de la grammaire), et se termine par les feuilles (terminaux). En
revanche, avec la stratégie ascendante, la construction de l'arbre démarre par les
feuilles et s'achève au niveau de la racine.
Chacune des deux stratégies appartient à une famille ou classe d'analyseurs. Là
précisément, on parle aussi de deux catégories d'analyseurs, à savoir, celle des
analyseurs non déterministes et celle des analyseurs déterministes. Dans la
première catégorie, les analyseurs sont fondés sur une classe (ou famille) assez
224 Chapitre 6
large de langages (ou grammaires) à contexte libre qui ne sont pas l'objet de
contraintes. Dans la deuxième catégorie, en revanche, les analyseurs s'appuient
sur des grammaires à contexte libre un peu spéciales satisfaisant certaines
conditions.
On présente ci-après une vue sommaire qui donne une idée sur la classification
des méthodes existantes d'analyse syntaxique. Ces méthodes se répartissent
d'abord en deux grandes familles qui elles-mêmes sont subdivisées en stratégies
(ascendantes et/ou descendantes). Le schéma de la Figure 118 donne une esquisse
sur la classification de ces méthodes.
Méthodes
Méthode de Méthode
ascendantes basées
Coke Younger d'Earley
sur des grammaires
Kasami
de précédence
Remarque 1.1
Le mot non déterministe attribué à une méthode vient de l'analyseur non
déterministe que l'on simule par un algorithme déterministe [Aho, 73].
Il convient de souligner que la classe des langages analysables avec les
méthodes non déterministes représente une famille beaucoup plus large que celle
des langages analysables par des méthodes déterministes. Les analyseurs non
déterministes, travaillant avec toutes les grammaires de type 2, se sont avérés
inefficaces pour être adoptés dans le développement de compilateurs,
contrairement aux analyseurs déterministes qui, eux, constituent une classe
privilégiée dans la construction de nombreux compilateurs existants.
Un analyseur non déterministe est considéré comme une heuristique.
Autrement dit, le processus d'analyse est caractérisé par une succession de succès
et/ou d'échecs intermédiaires jusqu'au succès ou l'échec final. Un analyseur
Analyse syntaxique 225
déterministe, quant à lui, possède un critère qui lui permet de savoir sans
équivoque quelle décision prendre à tout moment au cours du processus d'analyse.
Le domaine foisonne de méthodes et de techniques élaborées. Cependant, on
s'intéressera dans le cadre de cet ouvrage uniquement à quelques méthodes
d'analyse syntaxique parmi les plus répandues de la catégorie des analyseurs
déterministes.
Comme souligné ci-dessus, il existe deux approches principales pour les
analyseurs déterministes :
l'approche descendante qui travaille avec des grammaires dites LL ;
l'approche ascendante qui travaille avec des grammaires dites LR, ainsi que
des grammaires de précédence G P.
Pour des besoins d'optimisation, il est nécessaire parfois de combiner
l'approche ascendante avec l'approche descendante.
On explicitera plus en détails les termes LL et LR quand on introduira les
concepts de grammaires LL et LR. En attendant cependant, on note que les
lettres 'L' et 'R' viennent des termes left et right (qui signifient respectivement
gauche et droit en Anglais).
Comme convenu, étant donnée une grammaire G = (VN, VT, P, S); pour
vérifier si une certaine chaîne co appartient au langage L(G), on peut opter, soit
pour une stratégie d'analyse descendante, soit pour une stratégie d'analyse
ascendante. On trouvera de plus amples détails sur les définitions des stratégies
d'analyse dans le chapitre l.
Avec l'analyse descendante, on démarre à partir de l'axiome S et, par une
succession de dérivations, on tente de faire apparaître le mot co. Avec la stratégie
d'analyse ascendante, on démarre avec le mot co et on tente de remonter vers S
(axiome) par une succession de dérivations inverses nommées réductions. On ne
reviendra pas longuement sur ces concepts. Néanmoins, il convient de mettre en
évidence certains comportements singuliers de chacune des deux stratégies. On
rappelle à ce titre, que l'utilisation inappropriée de certaines grammaires peut
provoquer des effets indésirables au cours du processus d'analyse. Ainsi, si un
analyseur risque de boucler indéfiniment, la grammaire qui le sous-tend doit être
transformée de manière à contourner le problème de la boucle sans fin. Dans ce
cas:
Les analyseurs descendants doivent travailler avec des grammaires non
récursives à gauche et acycliques.
Les analyseurs ascendants doivent éviter de travailler avec des grammaires
cycliques.
Acyclique : signifie que la grammaire n'admet pas de dérivation de la forme
A=>+ A.
On rappelle que la récursivité à gauche implique qu'il existe des dérivations du
genre A=> *Aa.
Les grammaires cycliques peuvent effectivement engendrer des boucles infinies
au cours d'une analyse. En effet, étant donnée la grammaire cyclique définie par :
226 Chapitre 6
G = (VN, VT, P, S) où
VN= {S, A}
VT ={a, b}
P = {S ~ aS 1A1b;A~S1bA1 a}
et soit 11 ba 11 une chaine à analyser par cette grammaire. On suppose que l'on
effectue cette analyse en adoptant une stratégie descendante et que l'on impose un
ordre dans lequel doivent être utilisées les différentes règles. On suit ainsi l'ordre
dans lequel apparaissent ces règles dans G. On aura dans ce cas la règle S ~ aS
qui n'est pas satisfaisante et, du coup, on change d'alternative, car aS commence
par le caractère 11 a 11 qui ne coïncide pas avec le premier caractère 11 b 11 de la chaine
11 ba 11 • Le changement d'alternative consiste à essayer S ~ A. Le symbole A, à son
tour, sera utilisé avec sa première alternative qui est A ~ S, et c'est à ce niveau
que se produit la boucle infinie puisqu'il y aura également la règle S ~ aS qui ne
donnera pas satisfaction, et ainsi de suite, indéfiniment sans jamais pouvoir
s'arrêter. On rencontre quasiment le même problème avec l'analyse montante. En
effet, en reconnaissant le caractère 11 b 11 1 on l'empile, ensuite on le réduit au
symbole S, conformément à la règle S ~ b. On aura donc S dans la pile qui se
réduit à son tour en A selon la règle A ~ S. De même, en ayant A dans la pile, on
le réduit à S, conformément à la règle S ~ A, et ainsi de suite, sans jamais
pouvoir achever le processus d'analyse.
On définit le k-suivant de~ E (VT uVN)*, par l'ensemble Followk ({J) formulé
comme suit:
Followk ({J) = {w 1 S ~· 1 a{Jy et w E Firstk(Y)}
a et y E (VTUVN) *
M ~+TM 1E
T~FN
N~ * FN 1 E
F ~ (E) 1 a
Les résultats du calcul des ensembles First1 et Follow1 sont collectés dans le
Tableau XXXI.
A titre d'exercice d'application, on laisse le lecteur effectuer lui-même le calcul
de ces deux ensembles.
228 Chapitre 6
First1 Follow1
E a, ( ), e
M +, e ), e
T a, ( ), +, e
N *, e ), +, e
F a, ( ), +, *, e
pourra jamais savoir quelle est celle des deux alternatives (a.1 = aô ou a.2 = ay)
qui a été utilisée. Ceci conduit inévitablement à un non déterminisme (au sens
LL(l)). D'où la nécessité de satisfaire la condition First1 (a.1) n First 1 (Cl.2) = 0.
V A-+/; 1 E E P on a toujours First 1 (6) n Follow 1 (A) = 0
En effet, conformément à la définition formelle de Follow1 avec S =>*1 w{Jy =
wAy, il peut arriver que y =>* 1 aµ, et qu'on ait aussi à la fois A=> o =>* 1 UT/.
Dans ce cas, la troisième condition ne sera pas satisfaite, puisqu'on ne saura pas si
le caractère a provient de la dérivation y =>* 1 aµ ou de la dérivation o =>* 1 UT/.
Remarque 2.1
Dans le cas où il n'y a pas de règles du type A-+ o 1 E, la grammaire LL(l) sera
dite SLL(l) (ou simple LL(l)). La satisfaction de la deuxième condition suffit,
dans ce cas, pour affirmer qu'une grammaire non récursive à gauche est une
grammaire simple LL(l).
On calcule les Follow1 pour une règle uniquement si elle admet comme
alternative une e-production comme c'est le cas de M et N de l'exemple
précédent (voir Tableaux XXXI et XXXII). Autrement dit, la
condition First1 (o) n Follow1 (A) = </J, n'est nécessaire que pour les règles du
genre A -+ o 1 E.
En pratique, tout élément e E Follow1 sera remplacé par un marqueur de fin
d'analyse. A cet effet, on peut utiliser par exemple le symbole spécial $.
Ainsi, le tableau des ensembles First 1 et Follow1 présenté plus haut est
remplacé par le Tableau XXXII.
First1 Follow,
E a, (
M +, E ), $
T a, (
N *, E ), +, $
F a, (
M ~+TM 1 E
T-> FN
N ~ * FN 1 E
F ~ (E) 1 a
est-elle LL(l) ?
En appliquant scrupuleusement les trois conditions précédentes, on obtient les
résultats suivants :
230 Chapitre 6
La grammaire n'est pas récursive à gauche, car elle ne présente aucune règle de
la forme A ~ Aa
A-t-on First 1 (a 1 ) n First1 (a 2 ) = 0?
F ~ (E) 1 a: First 1 ((E)) n First1 (a) = {(} n {a}= 0
T ~ FN: First1 ((E)N) n First1 (aN) = {(} n {a}= 0
E ~TM: First1 ((E)NM) n First 1 (aNM) = {(} n {a}= 0
M ~+TM 1 e: First 1 (+TM) n First 1 (e) = {+} n {e} = 0
N ~ * FN 1 e: First 1 (* FN) n First1 (e) = {*} n {e} = 0
A-t-on First 1 (o) n Follow1 (A) = 0 ?
M ~+TM 1 e: First1 (+TM) n Follow1 (M) = {+} n {),e} = 0
N ~ * FN 1 e: First1 (* FN) n Follow1 (N) = {*} n {),e, +} = 0
Les trois conditions sont satisfaites, alors la grammaire est LL(l).
On peut également, si on le souhaite, appliquer le théorème, c'est-à-dire
V A-+ a 1 1 a 2 E P First 1 (a 1 .Follow1 (A)) n First 1 (a 2 .Follow1 (A)) = 0.
Dans ce cas, la grammaire définie par les règles de production de l'ensemble
suivant est-elle LL(l) ?
S ~
aABe
A~ Abc 1 b
B~d
D --7 E. 1 bcD
avec S comme axiome.
Pour vérifier si la grammaire obtenue est LL(l), on peut appliquer le théorème
(on peut tout aussi appliquer les trois conditions comme pour l'exemple
précédent).
Les trois premières règles, ont une seule alternative chacune ; il n'est donc pas
nécessaire de calculer leurs First 1 et Follow1 . En revanche, il est nécessaire de
calculer First1 et Follow1 pour la règle D --7 f. 1 bcD
First1 (c. Follow1 (D)) n First 1 (bcD. Follow1 (D)) =?
S => aABe => abDBe => abDde => abbcDd.
Donc Follow1 (D) = {d}.
Par conséquent, First1 (E.Follow1 (D)) n First 1 (bcD.Follow1 (D)) = {d} n {b} = 0
Ainsi, on déduit que la grammaire transformée est bien LL(l).
A~ aAb I 0
B ~ aBbb l 1
n'est pas LL(k) V k;::; 1.
En effet, intuitivement, si on commence à scanner une chaine formée de lettres a,
on ne sait pas laquelle, des alternatives S ~ A et S ~ B, a été utilisée jusqu'à
rencontrer soit 0, soit 1. Mais, conformément à la définition de grammaire LL(k),
donnée ci-dessus, il est possible de prendre w = a.= e, ~ = A,"(= B, x = lobk et
y= aklb2k dans les deux dérivations (1) et (2) suivantes :
(1) S ==>Olm S =>lm A==>* akObk
(2) S ==>Olm S==>lmB ==>* aklb2k
Remarque 2.2
La grammaire G n'est pas LL (k) V k;::; 1.
Le langage L(G), quant à lui, est analysable de manière descendante (gauche)
déterministe par un automate à pile déterministe.
L'automate de la Figure 119 est un analyseur très puissant doté d'une
mémoire (pile) qui lui permet de se rappeler le nombre de lettres a pour les
faire correspondre au nombre de lettres b.
La notion LL incombe à la grammaire. On constate que d'après le diagramme de
transition de la Figure 119, on peut effectuer une analyse descendante
entièrement déterministe sans qu'il soit nécessaire de disposer d'une grammaire
LL. La contrainte LL de la grammaire impose à l'analyseur de savoir quelle est la
règle qu'il va falloir appliquer à tout moment afin que l'analyse soit déterministe.
L'avantage de la grammaire relativement à l'automate à pile est qu'il est toujours
possible d'associer aux règles de production des règles de traduction ou des
schémas de traduction dirigée par la syntaxe. A l'inverse, l'automate à pile n'offre
pas toujours cette possibilité, en particulier s'il est construit intuitivement (sans
l'appui des règles de production). Il peut cependant être adopté dans des cas de
traducteurs un peu spéciaux notamment pour sa rapidité d'exécution.
Dans ce contexte (voir Figure 120), il a été rapporté dans [Aho, 73] (tome 2)
qu'il existe des grammaires qui ont les caractéristiques suivantes :
LR et permettent une analyse gauche (left-parsable), mais ne sont pas LL.
LR mais ne permettent pas d'analyse gauche (not left-parsable).
Permettent une analyse gauche et droite, mais non LR.
Permettent une analyse droite, mais non LR et ne permettent pas d'analyse
gauche.
234 Chapitre 6
a/#(##)
b/#(E)
0/#(#)
b/#(#)
s, aaOb, #
q, aOb, #
q, Ob, ##
i, b, ##
i, E, #
Le mot a été entièrement scanné et la pile n'est pas vide. Par conséquent, le mot
aaOb n'est pas accepté.
s, aalbbbb, #
q, albbbb, #
q, lbbbb, ##
t, bbbb, ##
p, bbb, ##
t, bb, #
p, ~ #
t, E, E
Le mot a été entièrement scanné et la pile est vide. Donc, le mot aalbbbb a été
accepté.
Analyse syntaxique 235
s, albbb, #
q, lbbb, #
t, bbb, #
p, bb, #
t, b, e
Le mot n'est pas entièrement scanné et la pile est vide. Autrement dit, le mot n'a
pas été accepté.
Le diagramme de la Figure 120 illustre les relations entre sous-classes de
grammaires à contexte libre [Aho, 73].
LR
Left
Parsable
c:J a
b
Right
Parsable
c d
contexte, sont tous les éléments de VT u {$}. Le symbole $ est utilisé à la fois
comme marqueur de fin d'analyse et comme fond de pile.
Pour définir les relations de précédence d'opérateurs, il convient tout d'abord
d'introduire certains concepts. On définit à ce titre, deux ensembles fondamentaux
nommés Firstop et Lastop qui s'expriment comme suit :
Firstop(X) ={a E VT 1 X::::)+ ya~, y E VN u {E} et~ EV*}
A titre indicatif, pour calculer, par exemple, Firstop (E), il va falloir utiliser la
dérivation indirecte positive E ::::) + ya~ qui produit graduellement les valeurs
de Firstop (E) comme suit : E ::::) E + T donne le symbole '+', E ::::) T::::) T * F
donne le symbole *, E::::) T::::) F::::) (E) donne le symbole parenthèse ouvrante (,
enfin E ::::) T ::::) F ::::) a donne le symbole a. En somme, on obtient Firstop (E) =
{+, *• (, a}. Les valeurs des ensembles Firstop et Lastop sont collectées dans le
Tableau XXXIII. Ces valeurs sont utilisées pour établir les relations de
Analyse syntaxique 237
Firstop Lastop
E a,+,*, ( a,+,*,)
T a, *, ( a,*,)
F a, ( a,)
a + * $
Associativité ~ ~
L opérateur * possède une
à gauche des <::;: ~ ~ priorité plus élevée que
opérateurs * <::;: ~ ~
celle de l'opérateur +, V
et+ sa position dans une
<::;: <::;: <::;: <::;: - expression
~ ~ ~ ~
Par exemple, soit à calculer EFF2 (S) de la grammaire définie par les règles
suivantes :
S ~AB
A~ Ba 1 e
B ~ Cb 1 C
C ~c 1 e
Le calcul donne, conformément à la définition de EFFk(a), l'ensemble
EFF2 suivant: EFF2 (S) = EFF2 (AB) = EFF2 (BaB) = EFF2 (CbaB CaB) = 1
{cb, ca}
@o = Vi(E)
EFF1 (SaSblS) = r/J
[S'~.S, E] et on a E et a fi. r/J, donc pas de conflit
[S~.SaSb, E]
[S~., E]
[S~.SaSb, a]
[S~., a]
@1 = V1 (S) = GOTO (@ 0 , S)
[S'~S., E]; [S~S.aSb, E]
240 Chapitre 6
@ 4 = V1 (SaSa) = GOTO (@ 3 , a)
[S~Sa.Sb, b]
[S~Sa.Sb, a]
[S~.SaSb, b]
EFF1 (SbblSbalSaSbblSaSba)= 0, mais a et b <t. 0
donc pas de conflit
[S~ .• b)
[S~.SaSb, a]
[S~., a]
Il n'y a que des items de la forme
@5 = V1 (SaSb) = GOTO (@ 3 , b) [A ---+p., u] et EFF1 (a)= {a} et e <t. {a},
[S~SaSb., E] donc pas de conflit
[S~SaSb., a]
@6 = V1 SaSaS) = GOTO (@ 4 , b)
[S~SaS.b, b] Il n'y a que des items de la forme
[S~SaS.b, a] [A ---+ p1 .p2 , u]. Donc pas de conflit
[S~S.aSb, b]
[S~S.aSb, a]
Il n'y a que des items de la forme [A ---+p., u].
@7 = Vi(SaSaSb) = GOTO (@ 6 , b) EFF1 (a)= {a} et b fi. {a},EFF1 (b) =
[S~SaSb., b) {b} et a <t. {b}.
[S~SaSb., a] Donc pas de conflit.
V1 (SaSaSa) = GOTO (@ 6 , a) = @4
Remarque 3.1
Il est possible d'implémenter un générateur de descentes récursives. Pour cela, il
faut d'abord mettre la grammaire LL(l) concernée sous forme d'un graphe
syntaxique. On utilise généralement une structure de liste chainée pour
implémenter ce type de graphe. Pour plus d'information sur ce sujet, le lecteur est
invité à réexaminer la notion de graphe syntaxique au niveau du chapitre 3.
Procédure E ;
début
T·1
M
fin;
Procédure T ;
début
F·1
N
fin;
Procédure M;
début si symbole = '+'
alors début
Accept ('+') ;
T;
M
fin
fsi
244 Chapitre 6
fin;
Procédure N ;
début si symbole = '*'
alors début
Accept ('*') ;
F;
N
fin
fsi
fin;
Procédure F;
début
si symbole = 'a'
alors Accept ('a')
sinon si symbole = '('
alors
début
Accept ( '(' );
E;
Accept ( ')' )
fin
sinon erreur ( )
fsi
fsi
fin ;
Procédure E;
début
Analyse syntaxique 245
écrire {1) ;
T·
M '
fin;
Procédure T ;
début
écrire {2) ;
F·
N '
fin;
Procédure M;
début
si symbole = '+'
alors début
écrire {3) ;
Accept ('+') ;
T;
M
fin
sinon si symbole Il: Follow1 (M)
alors erreur ( )
sinon écrire (4)
fsi
fsi
fin;
Procédure N ;
début
si symbole = '*'
alors début
écrire (5) ;
Accept ('*') ;
F;
N
fin
sinon si symbole Il: Follow1 (N)
alors erreur ( )
sinon écrire (6)
fsi
fsi
fin;
Procédure F;
début
si symbole = 'a'
246 Chapitre 6
alors début
écrire (7) ; Accept ( 'a')
fin
sinon si symbole = '('
alors
début
écrire (8) ; Accept ( '(' );
E; Accept ( ')' )
fin
sinon erreur ( )
fsi
fsi
fin;
Outre l'ajout des numéros des règles aux endroits prévus à cet effet, la
procédure M (resp la procédure N) a été légèrement modifiée dans le cas où ce
n'est pas le symbole '+' (resp le symbole '*') qui est rencontré. En effet, si le
symbole d'entrée rencontré est différent (:t) de tous les éléments de l'ensemble
Follow1 (M) (resp Follow1 (N)), une procédure de traitement des erreurs sera
systématiquement appelée. Dans le cas contraire, le numéro de la règle de
production 4 (dans la procédure M), sera émis en sortie (resp le numéro de la
règle de production 6 (dans la procédure N)).
Remarque 3.2
Il est évident que l'on peut reconduire la technique de la descente récursive pour
toute grammaire à contexte libre non récursive à gauche. Cependant, si la
grammaire n'est pas LL(l), la descente récursive n'a aucun intérêt pratique. En
effet, si le nombre de symboles k en prévision est supérieur à 2 (;::: 2), cela
augmente considérablement le nombre de tests permettant de distinguer entre
deux entités commençant par un même symbole, et la descente récursive devient
inutilisable.
Remarque 3.3
Les appels de procédures dans la descente récursive définissent implicitement
l'arbre syntaxique (arbre de dérivation). L'analyse du mot "a" par la descente
récursive fournit l'arborescence des appels qui n'est autre que l'arbre syntaxique
correspondant au mot "a" analysé. L'arborescence des appels du mot "a" est
schématiquement illustrée dans la Figure 122.
Remarque 3.4
Si le langage d'implémentation n'est pas récursif on est contraint de gérer les
appels récursifs par pile.
Une descente récursive est souvent lente à cause de la récursivité. Dans le cas
d'expressions (arithmétiques, logiques, etc.), une descente récursive est souvent
remplacée par un analyseur fondé sur la précédence d'opérateurs. On verra sous
peu, en quoi consiste un analyseur basé sur la précédence d'opérateurs.
Analyse syntaxique 247
n,=12746 ''
''
''
.li>t9f. . . ' . . '
Otq. ......... - - -
~ M
T
F
N
1 1
a e
a + * $
E TM, TM,
T
M +TM, (3) 4 e, 4
N e, 6 *FN, (5) 6 e, 6
F a, 7
a/ # (#)
$ / # (&) *
État final
)I# (r.)
Remarque 3.5
Par convention, on dirige le sommet de la pile d'analyse vers la droite, car il s'agit
d'une analyse ascendante. Pour rappel, l'analyse ascendante est représentée par
l'image miroir de la dérivation canonique droite.
Le principe est toujours celui de l'analyse ascendante (décaler/réduire). Mais,
il existe diverses techniques pour faire tourner ce type d'analyse.
On peut utiliser la grammaire squelette équivalente à la GPO qui a produit la
table des relations de précédence d'opérateurs (le but étant de raccourcir le
processus d'analyse) ;
On peut également utiliser l'algorithme qui n'utilise que les relations de
précédence, sans la grammaire comme l'algorithme suivant :
Positionner le pointeur ps sur le 1er symbole de la chaine d'entrée w
Tant que w (ps) -:F $ou pile (sommet) -:F $
faire
si pile (sommet) < ou= w (ps) /*relation a< ou= b */
alors début /* décalage */
empiler ( b) ;
avancer ps /* ps := ps +1 */
fin
sinonlsi pile (sommet) > w (ps) /* relation a > b */
alors répéter
dépiler (x)
jusqu'à pile (sommet) < x /*a<x*/
inon erreur ( ) ~
~ I
:
fsi I
I
Remarque 3.6
La grammaire définie par P' = {S ~ B + S 1 B; B ~ C * B 1 C; C ~ (S) 1 a}
est aussi une grammaire équivalente, sauf qu'elle n'est pas récursive à gauche. La
récursivité à gauche ne constitue pas un frein pour l'analyse ascendante
contrairement à l'analyse descendante.
L'analyse est conduite selon la séquence des pas suivants :
$ a $ <:: a shift
$a * a::> * reduce règle 4
$S * $ <:: * shift
$S* * <:: ( shift
$S*( a ( <:: a shift
$S*(a + a J> + reduce règle 4
Analyse syntaxique 253
Remarque 3.7
La table des relations de précédence d'opérateurs occulte (cache) certains cas
d'erreurs. Il est donc recommandé de créer un moyen exprimant les relations
erronées entre terminaux dans une même expression. A titre indicatif,
l'expression 11 a*+a 11 est syntaxiquement incorrecte, mais la table de précédence
ne signale pas d'erreur, car il y a à priori une relation de précédence entre les
opérateurs * et +. Ainsi, plutôt que de signaler une erreur, la table indique
qu'il faut effectuer une réduction qui va s'avérer plus tard être une erreur,
puisque l'opérateur + ne doit pas suivre immédiatement l'opérateur *. Il va
donc falloir ajouter une information qui va empêcher d'effectuer une opération
avant d'être sûr que rien ne l'interdit. On peut créer, par exemple, un
automate d'état finis un peu spécial qui exprime les relations non conformes
(erreurs) entre terminaux dans une même expression. On discutera de ce
problème dans le cadre de traitement des erreurs syntaxiques ultérieurement
dans ce chapitre.
Par ailleurs, il est possible de compresser la table des relations en introduisant
deux fonctions f et g exprimant la priorité à gauche et la priorité à droite telles
que /(.a)< g( b) si a< b, /(.a)= g( b) si a= b; /(.a) ::> g( b) si a::> b. Il faut noter
que /(. x) ou g( x) correspond à un entier représentant la priorité numérique (le
poids de l'opérateur). On parle de /(.a) lorsqu'un élément (l'opérateur a) est
dans la pile. On parle de g( b) lorsqu'un élément (l'opérateur b) est dans la
chaine (non encore analysée).
L'exemple qui va suivre a pour finalité de montrer comment compresser la
table des relations de précédence d'opérateurs en la remplaçant par les deux
nouvelles fonctions f et g évoquées ci-dessus. On expliquera notamment comment
on va remplacer les relations de la table de précédence ( <, =, ::>) respectivement
254 Chapitre 6
par des relations numériques (<, =, > ). Autrement dit, il s'agit d'attribuer des
poids aux opérateurs (et opérandes) dans une expression.
Soit alors la grammaire définie par les règles :
E~E+TIT
T~T*FIF
F~ a.
Les ensembles Firstop et Lastop ainsi que la table de précédence sont données
respectivement dans les Tableaux XXXV et XXXVI.
Firstov Lastov
E +,*,a +,*,a
T *,a *,a
F a a
a + * $
a )> )> )>
Approche algorithmique
Soient a et b deux opérateurs au sens relation de précédence d'opérateurs, c'est-à-
dire que a et b e VT u {$}. On initialise à 0 deux vecteurs nommés
respectivement leftprec et rightprec, et indexés par les éléments de l'ensemble
VT u {$}. Le calcul des priorités numériques ou pondérées sera accompli
algorithmiquement en appliquant les points suivants :
Si a J> b et leftprec [a] ~ rightprec [b], alors leftprec [a] f- leftprec [a] + 1 ;
Si a == b et leftprec [a] -:t. rightprec [b], alors il faut incrémenter le plus petit
des deux ; c'est-à-dire si leftprec [a] < rightprec [b], alors
leftprec [a] f- leftprec [a] + 1 sinon rightprec [b] f- rightprec [b] + 1 ;
Analyse syntaxique 255
Si a <:: b et leftprec [a] 2:: rightprec [b] alors rightprec [b] f- rightprec [b] + 1 ;
Les vecteurs leftprec et rightprec correspondent à ce que l'on a nommé ci-
dessus, les fonctions f et g. Donc, au lieu d'avoir une table NxN on aura une table
Nx2, ce qui est relativement une bonne optimisation de l'espace pris par la table.
Ainsi, conformément à cet algorithme et sur la base de la table des relations du
Tableau XXXVI, on obtient les valeurs des leftprec et rightprec répertoriées
dans le Tableau XXXVII.
a~~~
+ 2 1
* 4 3
$ 0 0
Tableau XXXVII - Table des priorités pondérées issues de la table de
précédence du Tableau XXXVI et de l'algorithme de calcul précédent
Soit:
E ~ E + E (i) 1 E * E (2) 1 a (3)
la grammaire squelette (ambiguë) équivalente à la grammaire définie ci-dessus
par les règles :
E~E+TIT
T~T*FIF
F~a
$E $ $= $ acceptation stop
La dérivation canonique droite est donc 1tr = 1 2 3 3 3. En effet, en utilisant cette
dérivation on retrouve l'expression 11 a + a * a" présentée en entrée, de la manière
suivante : E (l) => E + E (2) => E + E * E (3) => E + E *a (3) => E + a * a (3) =>
a+a*a.
f g
( [QTI]
) [ill]
Tableau XXXVIII - Table des priorités pondérées des parenthèses
ouvrante et fermante issues de la table de précédence de la grammaire des
expressions arithmétiques simples parenthésées et de l'algorithme de calcul
précédent : Approche algorithmique
+*, qui sont des erreurs syntaxiques mais non considérées comme telles dans la
table en question.
Après avoir répertorié tous les cas d'erreurs on construit une table dite d'états
d'une expression (automate d'états finis spécial indiquant comment doivent se
succéder les opérateurs et opérandes dans une expression). On reviendra sur cette
question, comme prévu, un peu plus loin dans le cadre du traitement des erreurs
syntaxiques.
A présent, en s'appuyant sur un exemple concret, on donne un aperçu de
l'approche basée sur la notion du plus long chemin dans un graphe biparti.
Le calcul du plus long chemin est réalisé en dessinant un graphe biparti et en
calculant manuellement la longueur du chemin à partir de chaque sommet du
graphe. On peut tout aussi implémenter le graphe par une matrice d'adjacence et
établir algorithmiquement la valeur du chemin le plus long, en s'appuyant sur
l'information recueillie à partir de la matrice en question.
La Figure 124 donne le graphe biparti qui exprime la précédence d'opérateurs
en utilisant la notion d'arc rentrant et d'arc sortant pour la grammaire définie par
les règles suivantes :
E~E+TIT
T~T*FIF
F~a
a~~~
+ 2 1
* 4 3
$ 0 0
a + * $
a 1 1 1
+ -1 1 -1 1
* -1 1 1 1
$ -1 -1 -1
Remarque 3.8
Quelle que soit la démarche suivie, parmi toutes celles exposées jusque-là, le
problème des erreurs reste un problème ouvert qui sera résolu en fonction des
différents cas d'erreurs. Il y a certains cas d'erreurs qui sont récoltés directement à
partir de la table. Les cas qui ne figurent pas dans la table doivent être collectés
afin de prévoir les procédures adéquates pour leur traitement. On se penchera sur
cette question quand on abordera le problème des erreurs syntaxiques.
Dans ce qui suit, on s'intéressera, comme prévu, à la classe la plus large de la
famille des analyseurs déterministes, c'est-à-dire, la classe des grammaires LR (k).
Analyse syntaxique 259
C = {@0, @ 1, ... , @1} est ensuite utilisée par l'algorithme ci-dessus pour générer la
table d'analyse LR du Tableau XLI.
Avant de présenter l'algorithme d'analyse LR qui est commun à toutes les
grammaires LR (LR, SLR, LALR, etc.), il est intéressant de se pencher d'abord
sur les particularités qui distinguent les grammaires LR des autres variantes de
grammaires LR, à savoir les grammaires SLR et LALR.
a b $ s Rj : réduction N° j
0 R2 R2 1 ..\
1 D2 Accepter
\
\
\
\
Dj : décalage, et transition
2 R2 R2 \ vers l'état j
3 - \
3 D4
' ', \
D5 ''
\
\
4 R2 R2 ',
_____
\
\
-:_~.),
6 ""- N° d'états vers
5 Rl Rl lesquels il y a une
6 D4 D7 transition
7 Rl Rl
Remarque 3.9
Lorsque k = 0, cela signifie que le nombre de caractères en pré lecture (ou pré
vision) est nul. En d'autres termes, dans un item LR noté [A~a..p, u], la chaine u
est toujours égale à la chaine vide e. De ce fait, puisque tous les items ont la
même chaine u égale à e, il devient alors inutile de réécrire celle-ci à chaque fois.
Donc, plutôt que de noter un item par [A~a..p, e], il est préférable d'utiliser le
format [A~a..p] nommée « cœur de l'item». Par ailleurs, si dans un ensemble
deux items ont le même cœur, on adopte l'écriture condensé [A~a..p, u 1 v], au
lieu de [A~a..p, u] et [A~a..p, v].
Remarque 3.10
Une conséquence directe du théorème (CNS) pour qu'une grammaire soit LR(k)
avec k ~ 0, indique qu'une grammaire Gest LR(O) si et seulement si chaque @i ne
contient que des items de réductions et aucun conflit entre ces réductions, ou bien
si chaque @i ne contient que des items de décalage. Un modèle d'item indiquant
une réduction serait donc de la forme [A~a..]. Un modèle d'item indiquant un
décalage serait de la forme [A~a..p].
Si une des conditions suivantes est satisfaite dans chaque ensemble @i calculé :
(1) ~ :;t: e et 8 :;t: e
{2) ~ = e et 8 :;t: e avec Followk(A) n EFFk (8.Followk(B)) = (/)
(3) ~ :;t: e et 8 = e avec Followk((B) n EFFk (~.Followk(A)) = (/)
{4) ~ = e et 8 = e avec Followk(A) n Followk(B) = (/)
Alors la grammaire G sera dite SLR{k) {Simple LR {k) pour tout k ~ 0).
A titre d'exemple, on donne la grammaire G définie par les règles :
s~c 1 D
c ~ac 1 b
D~aDlc
La condition (4) est satisfaite, de plus il y a qu'un seul item, et il ne peut pas se
contredire, donc pas de conflit.
@3 = GOTO (@o, D) = S ~ D.
La condition (4) est satisfaite, de plus il y a qu'un seul item, et il ne peut pas se
contredire, donc pas de conflit.
@4 = GOTO (@o, a)
@4 = C~a.C
D~a.D
c~.ac
c~.b
D~.aD
D~.c
La condition (1) du théorème est satisfaite, donc pas de conflit.
@s = GOTO (@o, b) = C ~ b.
La condition (4) du théorème est satisfaite, de plus il y a qu'un seul item, et il ne
peut pas se contredire, donc pas de conflit.
@5 = GOTO (@o, c) = D ~ c.
La condition (4) du théorème est satisfaite, de plus il y a qu'un seul item, et il ne
peut pas se contredire, donc pas de conflit.
@1 = GOTO (@4, C) = C~ac.
Remarque 3.11
Pour ce qui est de la construction de la table d'analyse SLR, on procède
quasiment de la même manière qu'avec la construction de la table d'analyse LR.
Les seules différences relevées résident dans le calcul de la collection des ensembles
d'items, ainsi que dans la forme des items eux-mêmes. En effet, au lieu de calculer
la collection canonique LR (1), on calcule la collection canonique LR (0). Donc,
par voie de conséquence, les items sont de fait de la forme (A~a..~] au lieu d'être
de la forme (A~a..~, u].
Analyse syntaxique 263
Grammaire LALR
La manière de définir une grammaire LALR, n'est pas aussi formelle que celle de
grammaire LR ou SLR. On peut toutefois s'appuyer sur des exemples concrets en
explicitant progressivement la notion de grammaire LALR. On mettra
particulièrement l'accent sur les spécificités qui la distinguent de ses homologues
LR et SLR.
Remarque 3.12
Comme annoncé précédemment, l'analyseur LR est commun à toutes les
grammaires de la famille LR. Il faut noter cependant, qu'un analyseur LR
utilisant une table SLR (1), par exemple, est aussi appelé analyseur SLR (1). Il en
sera de même pour le cas LALR ( 1).
Remarque 3.13
La Figure 125 donne un aperçu sur l'agencement hiérarchique des sous-classes de
la famille des grammaires LR.
On peut déduire, conformément à cette hiérarchie, que toute grammaire
LR (0) est nécessairement SLR (1), LALR (1) et LR (1). On utilisera cette
propriété très utile en pratique. En effet, très souvent, pour des besoins
d'optimisation, on adopte une méthode SLR ou LALR, plutôt qu'une méthode
LR. On pourra vérifier cette propriété moyennant certains exemples qu'on traitera
au fur et à mesure.
L~ *R
L~a
R~L
LR (1)
LALR (1)
r
SLR (1)
@i = GOTO (@o, S)
@i = s·~s.
@2 = GOTO (@o, L)
@2= S ~ L. = R
R~L.
réduction) à la lecture du symbole 11 =11 • Ce genre de conflit peut être résolu avec
la méthode LALR. L'idée, avec LALR, consiste à observer la façon dont chaque
état (ensemble d'items) est atteint, et d'établir, en conséquence, le contexte de
manière sélective. En d'autres termes, cela se traduit par la fusion des états ayant
le même cœur. Comme indiqué précédemment, le cœur correspond à la première
composante d'un item. Dans l'item [A~a.~, a], A~a.~, est le cœur de l'item en
question.
Cette fusion est très bénéfique en pratique, puisqu'elle diminue considérablement
le nombre d'états (exprimé en nombre de lignes de la table d'analyse LALR),
comparativement aux tables d'analyse LR ou SLR. L'exemple suivant donne une
idée précise sur l'avantage qu'offre la méthode LALR par rapport aux méthodes
SLR et LR.
La grammaire définie par les règles
s~cc
C~cCld
est-elle SLR{l) ?
Pour vérifier la condition SLR {1), on augmente d'abord la grammaire, ensuite
on construit la collection canonique LR{O). On contrôle également en parallèle si
les ensembles d'items LR déjà calculés sont consistants (non contradictoires).
Remarque 3.14
D'après le théorème sur la condition SLR{k), k ~ 0, on ne doit s'intéresser qu'aux
ensembles qui contiennent au moins deux items dont au moins un d'eux évoque
une réduction.
La grammaire augmentée est représentée par {S' ~ S; S ~CC; C~ cC 1 d}.
On construit alors la collection canonique des ensembles d'items LR{O) associée.
@o = s·~.s
s~.cc
c~.cc
c~.d
Les ensembles d'items @i, @4, @5 et @6 correspondent à des états finals. Les
états obtenus en transitant par @o, @2 et @3 sont des états qui se répètent, donc
on s'arrête. On a obtenu au total sept ensembles d'items correspondant à sept
états. Chaque état représente une ligne en termes de table d'analyse.
On déduit que la grammaire est SLR (1), puisque on ne rencontre pas
d'ensembles contradictoires en appliquant le théorème de grammaire SLR (k)
k ;;::: O. D'ailleurs, chaque ensemble rencontré comporte, soit un seul item évoquant
une réduction, soit plusieurs items ne correspondant qu'à des décalages.
Maintenant, on suppose que l'on veuille plutôt montrer que cette grammaire
est LALR (1). Donc, au lieu de calculer la collection LR (0), on calcule la
collection LR (1). On appliquera le théorème de grammaire LR (k) pour k = 1.
On notera le nombre d'états obtenus avec la méthode LALR pour le confronter à
celui obtenu avec les approches LR et SLR.
@o = {[S'~.s. e] ; [S~.cc, e] ; [C~.cc, c 1 d] ; [C~.d. c 1 d]}
Pas de conflit, car il n'y a que des items qui évoquent des décalages, donc
l'ensemble est non contradictoire ;
@1 = GOTO (@o, S) = {[S 1 ~S. 1 e]}
Possède un seul item, donc pas de conflit ;
@2 = GOTO (©o, C) = {[S~C.C, e] ; [C~.cC, e] ; [C~.d, e]}
Pas de conflit, car il n'y a que des items qui évoquent des décalages, donc
l'ensemble est non contradictoire ;
@3 = GOTO (©o, c) = {[C~c.C, c 1 d] ; [C~.cC, cld] ; [C~.d, c 1 d]}
Pas de conflit, car il n'y a que des items qui évoquent des décalages, donc
l'ensemble est non contradictoire ;
@4 = GOTO (@o, d) = {[C~d., c 1 d]}
Contient deux items de réduction qui ne sont pas en conflit, car c :;:. d. Donc,
l'ensemble n'est pas contradictoire ;
Analyse syntaxique 267
exemple ci-dessus dont l'ensemble des règles est P' = {S' ~ S(o); S ~ cc( 1l ; C
~ cd2) 1d(3)}.
c d $ s c
0 D3 D4 1 2
1 Accepter
2 D3 D4 5
3 D3 D4 6
4 R3 R3 R3
5 Rl
6 R2 R2 R2
Tableau XLII - Table d'analyse SLR (1) pour la grammaire S' ~ S(o);
s ~ cd 1l ; c ~ cd2l 1d( 3)
c d $ s c
0 D3 D4 1 2
1 Accepter
2 D6 D7 5
3 D3 D4 8
4 R3 R3
5 Rl
6 D6 D7 9
7 R3
8 R2 R2
9 R2
c d $ s c
0 D3-6 D4-7 1 2
1 Accepter
2 D3-6 D4-7 5
3-6 D3-6 D4-7 8-9
4-7 R3 R3 R3
5 Rl
8-9 R2 R2 R2
Tout compte fait, la construction de la table d'analyse LALR {1) révèle que
l'on aboutit exactement à la même table d'analyse que celle obtenue avec la
méthode SLR {1), il suffit de remplacer 3-6, 4-7 et 8-9, respectivement par 3, 4 et
6. Ceci confirme l'avantage de la méthode LALR par rapport à la méthode LR. En
effet, la table d'analyse LALR comporte moins de lignes que son homologue LR.
Avant de clore le volet concernant l'analyse syntaxique basée sur la famille des
grammaires LR, on décrit succinctement l'algorithme d'analyse qui, pour rappel,
est commun à toutes les grammaires LR, SLR et LALR. L'algorithme en question
interprète les différentes commandes indiquées dans la table d'analyse LR, SLR ou
LALR. Les abréviations D et R, par exemple, sont des commandes indiquant
respectivement un décalage et une réduction. Plus précisément, quand on écrit par
exemple Dj, cela signifie qu'il faut effectuer un décalage, c'est-à-dire « empiler » le
symbole rencontré en entrée, ensuite enchainer l'action en transitant vers l'état
{ligne) numéro j.
08la283a486b7 b$ Rl
08la283 b$ D5
081a283b5 $ Rl
081 $ « Accepter »
La chaine "aabb" a été acceptée en laissant la trace d'analyse montante : R2, D2,
R2, D4, R2, D7, Rl, D5, Rl, c'est-à-dire la séquence des règles 2 2 2 1 1. Ce qui
correspond à la dérivation canonique droite en utilisant la séquence des règles de
réduction en sens inverse 1tr = 1 1 2 2 2. En effet, 8 :::}(l) 8aSb :::} {l) 8aSaSbb :::}
(2 ) 8aSabb :::} (2) 8aabb :::} (2 ) aabb. Ceci confirme la véracité de l'analyse droite
obtenue.
Etat Entrée Action
o, cdcd$, D3-6
Oc3-6, dcd$, D4-7
Oc3-6d4-7, cd$, R3
Oc3-6C8-9, cd$, R2
OC2, cd$, D3-6
OC2c3-6 d$, D4-7
OC2 c3-6 d4-7, $, R3
OC2c3-6C8-9, $, R2
OC2C5, $, Rl
081, $, « Accepter »
Egalement dans le deuxième cas, la chaine "cdcd" a été acceptée en laissant la
trace d'analyse montante : D3-6, D4-7, R3, R2, D3-6, D4-7, R3, R2, Rl, c'est-à-
dire la séquence des règles 3 2 3 2 1. Ce qui correspond à la dérivation canonique
droite en utilisant la séquence des règles de réduction en sens inverse 1tr = 1 2 3 2
3. En effet, 8 :::}(l) CC :::} (2) CcC :::} (3) Ccd :::} (2) cCcd:::} (3) cdcd. Ceci confirme
également la véracité de l'analyse droite obtenue.
On termine cette partie avec deux exemples particulièrement intéressants. Le
premier confirme qu'une grammaire LR (1) n'est pas toujours LALR (1). Le
deuxième montre qu'il est possible d'exploiter la priorité (précédence)
d'opérateurs d'une grammaire ambiguë pour qu'elle puisse être utilisée avec un
analyseur déterministe de type LR.
Par exemple, la grammaire définie par les règles suivantes est LR (1) mais non
LALR (1).
8' -7 8
8 -7 aAd lbBd 1 aBe 1 bAe
A-7 c
B -7 c
[S~.bBd, e]
[S~.aBc, e]
[S~.bAc, e]}
L'ensemble ne contient que des items de décalage, il n'y a aucun conflit à
déplorer;
@i = GOTO (@ 0, S) = [S'~S., E]}, l'ensemble contient un seul item, il ne
comporte donc pas de conflit ;
GOTO (@o, a)= @2
@2 = [S~a.Ad, e]
[S~a.Bc, e]
[A~.c, d]
[B~.c, c]
L'ensemble ne contient que des items de décalage, donc d'après le théorème, il n'y
a aucun conflit ;
GOTO (@o, b) = @3
@3 = [S~b.Bd, E]
(S~a.Ac, E]
[A~.c, c]
[B~.c, d]
Egalement cet ensemble ne contient que des items de décalage donc, ne présente
aucun conflit ;
GOTO (@2, c) = @4
@4 = [A~c., d]
[B~c., c]
L'ensemble contient deux items de réduction sans conflit. En effet, les deux
réductions auraient pu rentrer en conflit, car elles ont un cœur commun, mais il se
trouve que leurs deuxièmes composantes sont distinctes (c °I' d) ;
GOTO (@3, c) = @5
@5 = [A~c., c]
(B~c., d]
Ici également, l'ensemble contient deux items de réduction sans conflit. En effet,
les deux réductions auraient pu rentrer en conflit, car elles ont un cœur commun,
mais il se trouve que leurs deuxièmes composantes sont distinctes (c °I' d).
Le calcul est mené à terme, et tous les ensembles d'items sont consistants (sans
conflit). En conséquence, la grammaire est LR (1).
Cependant, si l'on essaie de fusionner les ensembles @4 et @5 (comportant des
cœurs communs), on rencontre un conflit du type réduction/réduction. En effet, si
on tente une réduction dans le nouvel ensemble @45 = {[A~c., cld], [B~c., cld]}
après la fusion, cette réduction est effectuée sur les mêmes symboles c et d qui
sont communs aux deux réductions A ~ c et B ~ c, dans l'ensemble @45 . Ceci
provoque un conflit qui montre que la grammaire n'est pas LALR (1).
Analyse syntaxique 273
E ~ .(E)
E ~.a}
@5 = GOTO (@2, E)
@5 = {E ~ (E.)
E ~ E. +E
E ~ E. * E}
@1 = GOTO (@4, E)
@1 = {E ~ E + E.
E ~ E. + E
E ~ E. * E}
@a= GOTO (@s, E)
@a= {E ~ E * E.
E ~ E. + E
E ~ E. * E}
@g = GOTO (@5,) ) = { E ~ (E).}
Donc, comme prévu, on n'a pas cherché à montrer en parallèle, comme
d'habitude, que la grammaire est LR (0). On voudrait plutôt prendre appui sur
cette collection afin de dresser la table d'analyse quand bien même cette dernière
serait multi définie.
a + * ( ) $ E
0 D3 D2 1
1 D4 D5 Accepter
2 D3 D2 6
3 R4 R4 R4 R4
4 D3 D2 7
5 D3 D2 8
6 D4 D5 D9
7 Rl/)(4 ~/D5 Rl Rl
8 R2/)(4 R2~ R2 R2
9 R3 R3 R3 R3
décalage. La réduction Rl est mise en relief par une trame de fond en gris dans la
table d'analyse. Sur la même ligne (état 7), c'est le décalage D5 qui est plutôt
exécuté avant la réduction Rl. Ceci est dû à la priorité de l'opérateur * qui est
toujours plus élevée que celle de +. Sur la ligne 8, c'est la réduction R2 qui est
prise en considération car quelle que soit la position de l'opérateur *, dans une
expression arithmétique comme 11 a * a + a" ou 11 a + a * a", c'est toujours la
multiplication qui s'exécute en premier avant l'addition. Ceci explique donc
parfaitement que, même ambigüe, une grammaire peut être utilisée en mettant de
l'avant les priorités et les associativités des opérateurs afin de résoudre les actions
conflictuelles au cours de l'analyse.
En l'occurrence, si on avait opté pour la grammaire non ambiguë équivalente
définie par les règles de l'ensemble P = {E ~ E + T (l) 1 T (2) ; T ~ T * F (3) 1 F
(4 ); F ~ (E) (5 ) 1 a (5 )}, on aurait montré que celle-ci est LR (1), et obtenu une
table d'analyse de douze lignes comme celle du Tableau XL VI. On peut
confronter les deux tables en question. On utilise à cet effet, une même expression
pour mettre en évidence les différences existantes entre les deux approches.
Soit alors à analyser la chaine 11 a + a * a", en utilisant successivement les
deux tables. On commence avec la table de la grammaire ambiguë.
Analyse de la chaine 11 a + a * a" avec la table d'analyse du Tableau XL V.
Etat Entrée Action
0 a+a*a$ D3
Oa3 +a* a$ R4
OEl +a* a$ D4
OE1+4 a* a$ D3
OE1+4a3 * a$ R4
OE1+4E7 * a$ D5
OE1+4E7*5 a$ D3
OE1+4E7*5a3 $ R4
OE1+4E7*5E8 $ R2
OE1+4E7 $ Rl
OEl $ « Accepter »
OE1+6T2*7F10 $ R3
OE1+6T9 $ Rl
OEl $ «Accepter»
En somme, il y a 13 pas d'analyse et une table d'analyse constituée de 12 lignes
avec la grammaire non ambiguë, alors qu'on obtient 10 pas d'analyse et une table
de 10 lignes avec la grammaire ambiguë équivalente.
a + * ( ) $ E T F
0 D5 D4 1 2 3
1 D6 Accepter
2 R2 D7 R2 R2
3 R4 R4 R4 R4
4 D5 D4 8 2 3
5 R6 R6 R6 R6
6 D5 D4 9 3
7 D5 D4 10
8 D6 Dll
9 Rl D7 Rl Rl
10 R3 R3 R3 R3
11 R5 R5 R5 R5
Niveau logique, comme le cas d'un appel récursif sans fin ou boucle infinie,
dans un programme, etc.
On peut également citer un autre type d'erreur qui dépasse le cadre du
compilateur. Ce type d'erreur est généralement lié à l'environnement dans lequel
opère un compilateur, comme par exemple l'insuffisance mémoire ou la capacité
limitée de la table des symboles, etc. Mais souvent, dans un compilateur, la part
la plus importante de la localisation et de la récupération sur erreur est centrée
autour de l'analyse syntaxique.
On s'intéresse ici au traitement des erreurs d'ordre syntaxique. Le traitement
des erreurs comprend en général deux volets principaux, à savoir leur détection,
ensuite leur gestion. La détection est systématique, puisqu'elle suit la syntaxe du
langage ou les règles de la grammaire. En revanche, la gestion est souvent basée
sur des procédures particulières. Il existe plusieurs stratégies de gestion des
erreurs et on en choisit souvent celle qui répond au mieux au contexte de l'erreur.
Quatre modes de recouvrement en cas d'erreur ont été proposés dans la littérature
des compilateurs [Aho, 86] :
Mode « panique » : C'est la stratégie la plus simple et la plus utilisée avec la
plupart des méthodes d'analyse syntaxique. Suite à la détection d'une erreur
l'analyseur adopte la stratégie qui consiste à ignorer une partie du flot d'entrée
(programme source), jusqu'à la rencontre d'un symbole de synchronisation fixé
à l'avance. Ce dernier peut être, par exemple, un symbole de ponctuation
comme la virgule, le point-virgule, etc., et permet au compilateur de repérer
l'endroit approprié afin de poursuivre l'analyse.
Mode « syntagme » : l'analyse syntaxique corrige localement l'erreur
rencontrée afin de permettre la poursuite de l'analyse. Par exemple, remplacer
la virgule ( 11 1 11 ) par le point-virgule ( 11 ; 11 ), détruire le point-virgule excédentaire,
insérer un point-virgule, etc.
Mode « règles de production d'erreurs » : Si on a une idée précise des erreurs,
on peut étendre l'ensemble des règles de production de la grammaire en
ajoutant des règles qui renferment des erreurs d'un certain type. Par exemple,
F~E) et F~(E sont deux règles qui engendrent formellement des expressions
syntaxiquement erronées. Par conséquent, en cas d'erreur (parenthèse ouvrante
ou fermante, manquante), le compilateur poursuit l'analyse tout en signalant
au passage les numéros de ces règles.
Mode « correction globale » : Dans l'idéal, il est souhaitable qu'un compilateur
effectue le moins de changements en cas d'erreurs. Il existe certains
algorithmes permettant de choisir une séquence minimale de changements
correspondant au coût de correction plus faible. Si x est un programme
incorrect et y son remplaçant correct, ces algorithmes cherchent le nombre
d'insertions et de suppressions minimum pour passer de x à y. Ces méthodes
ont une complexité très élevée en temps et en espace. Par ailleurs, le
programme y le plus proche de x peut ne pas correspondre à celui que
l'utilisateur avait l'intention d'implémenter. Cette stratégie reste d'un intérêt
théorique.
L'existence d'une erreur dans un programme fait réagir le compilateur qui la
signale par un message explicite à l'endroit approprié. Une erreur sur une entité
278 Chapitre 6
Remarque 4.1
Il existe des compilateurs qui interrompent le processus d'analyse dès la rencontre
d'une erreur. L'erreur est signalée par un message indiquant le type d'erreur ainsi
que l'endroit (procédure, numéro de ligne, etc.), où elle a été rencontrée. Quand
on la corrige, on peut recompiler le programme. Dans ce cas, il y aura autant de
compilations qu'il y a d'erreurs dans le programme. Ce type de compilateur n'est
pas très intéressant et n'a aucun succès en pratique aux yeux d'un professionnel.
En revanche, les compilateurs très professionnels dressent généralement un
inventaire des erreurs sous forme d'un rapport explicite. Ce rapport permet
d'aider les développeurs à corriger très rapidement leurs programmes.
P = {E ~TM (l)
T ~ FN (2)
M ~ +TM (3)1 E (4 )
N ~ *FN (5) 1 E (5)
F ~a (7) l{E) (s)}
a + *
E
T sync
M +TM, (3)
N E, 6 *FN, (5)
F a, 7 sync sync
de cette erreur à plus tard. Il existe des stratégies de récupération sur ce type
d'erreur, mais elles engendrent très souvent une grande perte de temps. Une
stratégie plus subtile consiste à prévoir l'erreur et la traiter en temps réel. Sa mise
en œuvre nécessite de disposer de l'information complète sur toutes les erreurs
susceptibles de se produire dans une expression. Pour cela, on collecte d'abord
tous les cas d'erreurs qui sont notés dans la table de précédence (cases vides),
comme c'est le cas de l'expression ") a * + a" ; ensuite on en rajoute ceux qui
sont occultés, comme par exemple le cas de l'expression "a + * a".
Erreurs répertoriées dans la table de précédence
On met en valeur la partie de la table de précédence concernée par ce type
d'erreurs. Cette partie de la table est représentée dans le Tableau XL VIII
[Aho, 86]. Les routines de traitement d'erreurs associées sont explicitées ci-
dessous.
a $
a E3 E3 :::> ;>
( <::: <::: - E4
) E3 E3 ;> :::>
$ <::: <::: E2 E1
Un opérateur additif comme + ou - ne doit être suivi que par un opérande "a"
ou une parenthèse ouvrante " (". Ceci est indiqué dans la table par les transitions
de l'état 0 à l'état 1 sur le symbole + ou -, ensuite de l'état 1 vers l'état 2 sur le
symbole a, ou vers l'état 0 sur la parenthèse ouvrante " (".
Enfin, à l'état 2, l'automate indique qu'on ne doit accepter ni opérande ni
parenthèse ouvrante. Autrement dit, tous les autres symboles ( +, -, *, / et Î)
sont valides.
Ainsi donc, cet automate constitue un complément d'information capital
permettant de gérer à bon escient les cas d'erreurs. Grace à son implémentation,
l'analyseur n'aura pas à revenir sur ses pas (retour arrière), et bénéficie d'un gain
substantiel en temps de traitement.
On finit cette section (traitement des erreurs syntaxiques) en testant un
exemple d'expression erronée, pour montrer qu'avec la technique de l'automate
comme celui du Tableau XLIX, l'analyseur ne perd pas de temps. L'erreur est
détectée et traitée en temps réel de manière fiable par l'analyseur.
Soit à alors à analyser la chaine "a * + a".
Outre les actions décaler/réduire (shift/reduce), qui existent naturellement
dans ce type d'analyseur, on intègre les différentes transitions dictées par
l'automate d'états du Tableau XLIX.
En ce qui concerne l'analyse proprement dite, on opte pour l'algorithme qui
n'utilise que les relations de précédence, sans la grammaire.
les liens des entités entre elles et avec leurs attributs respectifs. Les liens seront
établis pendant la phase d'analyse syntaxique qui va suivre logiquement l'analyse
lexicale.
En revanche, quand on a à faire à un compilateur qui regroupe l'analyse lexicale
et l'analyse syntaxique dans une même passe (cas des compilateurs actuels), la
liaison entre une entité et les informations (attributs) la concernant est établie
directement. Les déclarations sont généralement la source d'information qui
permet d'associer à une entité son type appropriée.
Par exemple, soit la déclaration d'une variable indicée en Pascal :
var x: array [l..10] of integer ;
Avec la première approche de compilation évoquée ci-dessus, le flot constituant
cette déclaration sera enregistré dans la table des symboles comme : "var", "x",
"array", "of", "integer", sans pour autant se pencher sur les liens existants entre
les différents mots composant la déclaration. Les détails concernant les autres
symboles et entités":","[","]", "1", " .. "et "10" ne sont pas mis de l'avant ici; le
but étant tout simplement de marquer la différence entre les deux approches. La
mise à jour de la table avec cette approche est laissée à la charge de l'analyse
syntaxique qui saura établir les liens entre toutes ces entités, en s'appuyant sur les
règles de production appropriées qui décrivent les déclarations en Pascal.
Avec la deuxième approche, en revanche, on utilise directement la règle de
production décrivant la déclaration. Les liens entre les différentes entités sont
établis en même temps que sont enregistrées ces dernières dans la table. En effet,
la règle de production contient suffisamment d'informations qu'il n'est pas difficile
d'exploiter pour indiquer que la variable x correspond à un tableau d'entiers. Les
autres informations "1 ", " .. " et "10", seront utilisées ultérieurement en analyse
sémantique et traduction pour calculer l'adresse du tableau x et réserver 10 places
permettant d'accueillir éventuellement 10 valeurs entières.
On a remarqué à travers cet aperçu que la table des symboles est toujours en
mouvement. Son activité nécessite de connaitre ce qui s'est déjà produit dans la
phase précédente et d'anticiper sur les prochaines phases. En l'occurrence, un
exemple illustratif mais très résumé concernant les mouvements et les activités du
processus de compilation sur la table des symboles est décrit dans la section 3 du
chapitre 4.
Ainsi, pour une exploitation efficace et cohérente de la table des symboles, il
est primordial d'avoir une maitrise totale de la gestion de la structure de données
choisie pour son implémentation.
Remarque 5.1
Pour mieux saisir l'importance et l'utilité de la table des symboles dans un
compilateur, l'utilisateur est invité à s'exercer en implémentant un analyseur pour
un petit noyau du langage de son choix (C ou Pascal), comportant au moins
quelques déclarations, des opérations arithmétiques et au moins une affectation.
Un analyseur couvrant la partie frontale du compilateur suffit pour avoir une
synoptique entre les entités du flot d'entrée et les entités générées en sortie avec
leurs références à la table des symboles. On peut implémenter ce noyau :
286 Chapitre 6
6 Exercice récapitulatif
On voudrait comparer certaines méthodes d'analyse syntaxique en s'appuyant sur
un exemple. Pour simplifier, on propose d'analyser syntaxiquement l'expression
arithmétique 11 a + a 11 •
Méthode générale (non déterministe)
Les méthodes générales n'ont pas été étudiées ici dans l'ouvrage car elles ne
présentent pas beaucoup d'intérêt dans l'écriture des compilateurs, vu leur
lourdeur. On propose de tester, la méthode générale descendante. On utilise alors
dans ce cas, une grammaire non récursive à gauche. Soit alors la grammaire : S ~
T + E(t)., E ~ T( 2)., T ~ F * T(3)., T ~ F(4) ·, F ~ a( 5) ·J F ~ (E)( 6)
L'algorithme utilise deux piles pour garder la trace de l'analyse, et permet, entre
autre, d'effectuer un retour arriêre pour redémarrer l'analyse à partir d'un certain
point.
On organise l'ensemble de sorte que les rêgles associées à E, T et F, soient
subdivisées en sous-ensembles d'alternatives. Par exemple, E possêde comme
alternatives E 1 et E 2 qui correspondent respectivement aux membres droits T + E
et T des rêgles de numéros (lJ et (2l. Il en est de même en ce qui concerne les
rêgles de T et F. Le principe de l'algorithme est d'essayer les alternatives dans
l'ordre. En cas d'échec d'une alternative, elle est remplacée par une autre.
L'algorithme complet se trouve dans le tome 1 de (Aho, 73]. Le but étant ici
uniquement de montrer l'efficacité des méthodes déterministes comparativement à
leurs homologues générales. La séquence d'analyse suivante montre l'application
de l'algorithme sur l'expression 11 a + a 11 •
Etat N° Pilel Pile2 Chaine
s, 1, E, E$ a+ a$
1- s, 1, Ei, T+E$ a + a$
1- s, 1, E1T1, F*T+E$ a+ a$
1- s, 1, E1T1F1, a*T+E$ a+ a$
a=a 1- s, 2, E1T1F1a *T+E$ +a$
+** 1- r, 2, E1T1F1a, *T+E$ +a$
retour 1- r, 1, E1T1F1, a*T+E$ a+ a$
alternative 1- r, 1, E1T1, F*T+E$ a+ a$
1- s, 1, E1T2, F+E$ a+ a$
1- s, 1, E1T2F1, a+E$ a+ a$
a=a 1- s, 2, E1T2F1a, +E$ +a$
+=+ 1- s, 3, E1T2F1a+, E$ a$
1- s, 3, E1T2F1a+E1, T+E$ a$
1- s, 3, E1T2F1a+E1T1, F*T+E$ a$
1- s, 3, E1T2F1a+E1T1F1, a*T+E$ a$
1- s, 4, E1T2F1a+E1T1F1a, *T+E$ $
1- r, 4, E1T2F1a+E1T1F1a, *T+E$ $
Analyse syntaxique 287
descente récursive aurait produit les mêmes numéros de règles (1t1), mais à la
288 Chapitre 6
différence, le processus aurait été beaucoup plus lent avec la descente récursive à
cause des nombreux appels récursifs.
Méthode LR
On reconsidère la table d'analyse SLR (1) (voir Tableau XL V) associée à la
grammaire représentée par E' ~ E (o) ; E ~ E + E (l) / E * E <2l / (E) (3) / a (4)
On obtient la séquence d'analyse LR suivante :
Etat Entrée Action
0 a+ a$ D3
Oa3 +a$ R4
OEl +a$ D4
~l~ a$ OO
OE1+4a3 $ R4
OE1+4E7 $ Rl
OEl $ Acceptation
En somme, il y a 6 pas d'analyse et une table d'analyse constituée de 10 lignes.
Les analyses effectuées avec les différentes méthodes révèlent la supériorité des
méthodes déterministes. D'où, leur préférence dans l'écriture des compilateurs et
traducteurs par rapport aux méthodes générales.
Le lecteur peut tester une ou plusieurs expressions de son choix. Il peut même
programmer ces différentes méthodes s'il désire approfondir ses connaissances en
la matière.
Chapitre 7
Traduction
1 Introduction
Au terme des phases de la partie frontale (lexicale, syntaxique et sémantique),
certains compilateurs construisent explicitement une représentation intermédiaire
considérée comme un programme pour une machine abstraite. Il existe une variété
de formes intermédiaires dont les plus répandues et les plus largement utilisées
sont l'arbre abstrait, la forme polonaise inverse (dénommée également forme post-
fixée) et le code à trois adresses. L'arbre abstrait se trouve être la forme la plus
générale, car il peut être interprété sur n'importe quel type de machine, il suffit
d'y associer l'algorithme adéquat pour son interprétation. Le code à trois adresses,
quant à lui, semble tirer son formalisme du langage d'assemblage d'une machine
dans laquelle chaque emplacement mémoire peut jouer le rôle d'un registre. Ce
type de code se prête particulièrement bien à une machine à registres,
contrairement à la forme post-fixée qui s'adapte plutôt mieux à une machine à
pile (on peut toutefois simuler le comportement d'une machine à pile sur une
machine à registres pour traiter du code post-fixé).
La partie finale constitue la synthèse du compilateur, c'est-à-dire la production du
code cible. Ce dernier peut être du code en langage d'assemblage qui est transmis
à un assembleur (traducteur assembleur) pour être traité de nouveau. Certains
compilateurs produisent eux-mêmes du code machine translatable qui est traité
directement par le relieur-chargeur. D'autres, génèrent du code exécutable.
On rappelle que la partie finale ne dépend généralement pas du langage source,
mais uniquement du langage intermédiaire et des caractéristiques de la machine
cible. La partie frontale, quant à elle, dépend principalement du langage source,
mais elle est indépendante de la machine cible.
290 Chapitre 7
2 Formes intermédiaires
Une forme intermédiaire :
Doit assurer une meilleure portabilité des compilateurs, c'est-à-dire qu'il sera
facile de changer le langage source ou le langage cible en adaptant la partie
frontale ou la partie finale.
Doit être facile à produire à partir du langage source et facile à traduire en
langage cible.
Il existe à peu près trois formes intermédiaires bien connues et couramment
utilisées par les compilateurs. A ce titre, on parlera de la forme post-fixée, l'arbre
abstrait et le code à trois adresses. Il peut exister d'autres formes intermédiaires
dites hybrides, mais on en parlera pas ici.
binaire abstrait comme celui de la Figure 127, les parcours pré-ordre (pour
obtenir la forme préfixée), et post-ordre (pour obtenir la forme post-fixée),
peuvent être exprimés récursivement par les formalismes suivants :
p Tl T2 : pour obtenir la forme préfixée.
Tl T2 p : pour obtenir la forme post-fixée.
Tl T2
2 /*' 4
//'
5 2
Pour des raisons évidentes comme, par exemple, la bonne gestion de l'espace
mémoire, c'est la représentation dynamique qui est plus à même de répondre au
mieux aux exigences des compilateurs actuels.
Cet exemple n'est donné ici qu'à titre d'illustration des deux approches de
représentation. Certains détails concernant les entités (opérandes et opérateurs)
sont omis délibérément.
1 2 3 4 5 6 7 1.. N
nœud - * 2 4 / 5 2
fils 2 3 0 0 6 0 0
gauche
fils droit 5 4 0 0 7 0 0
Pour rappel, un exemple très explicite, sur les détails concernant les entités
mises en jeu dans une expression arithmétique, a été proposé en section 3 du
chapitre 4. On donnera d'autres exemples concrets, sous peu, dans ce chapitre,
pour mieux approfondir et élucider la question de la génération de code
intermédiaire.
294 Chapitre 7
Remarque 2.1
Avant d'introduire un autre type de représentation intermédiaire, on voudrait
définir au préalable un formalisme permettant d'étendre la notation post-fixée à
d'autres constructions, que l'on rencontre souvent dans la plupart des langages de
programmation, comme les instructions de contrôle, les affectations, etc. On
utilisera, à ce titre, un formalisme très rigoureux et facile à interpréter (par un
interpréteur) ou à exploiter lors de la phase finale de génération de code cible par
un compilateur.
2 / / / 5 2 / /
Remarque 2.2
La représentation post-fixée est généralement destinée pour une machine à pile.
On peut toutefois simuler le comportement d'une telle machine sur une machine
classique ordinaire.
On se propose à présent de traduire l'instruction conditionnelle suivante,
ensuite de l'évaluer {simuler son exécution), en s'appuyant sur une machine à pile
virtuelle matérialisée par l'algorithme générique ci-dessus.
L'instruction conditionnelle est la suivante :
"if (a* b > 2) and (c = d) then x := x + 1 else x := x- 1 11
Sa forme post-fixée, conformément au formalisme développé ci-dessus, est la
suivante:
a b * 2220 BZ c d = 2f BZ x x 1 + : = ~J x 1 - J
Pour l'évaluation, on prend plutôt une instance de l'instruction, en remplaçant
respectivement a, b, cet d par les valeurs numériques 3, 2, 5 et 5. Ce qui donne le
code post-fixé suivant :
32 * 2- 20 BZ 5 5 = i° BZ X X 1+ : = l-~ X 1- :J
Ainsi, en appliquant l'algorithme sur le code post-fixé ci-dessus, c'est
l'affectation x := x + 1 qui est exécutée, ensuite on effectue un branchement vers
l'adresse 25, c'est-à-dire vers l'instruction qui vient immédiatement après
l'instruction conditionnelle considérée.
and
~<
/
b 5
Figure 130: Arbre abstrait de l'expression "(a* 2 > c) and (b < 5)"
Quant à la deuxième instruction, à savoir, < if_ then_ else >,c'est un peu moins
simple. En effet, un effort supplémentaire est nécessaire pour construire l'arbre
abstrait de l'expression en question. L'arborescence doit refléter sémantiquement
le contrôle exprimé par l'instruction< if_ then_ else > (Figure 131).
On doit faire en sorte que la transformation (traduction) de l'instruction sous
forme d'un arbre abstrait, tienne compte de l'exploitation future de celui-ci (lors
de la traduction en code cible). En effet, comme on sait que, seule une des deux
alternatives sera exécutée, soit "then x := x + 1", soit "else x := x - 1", alors il
va falloir créer un nœud artificiel (une sorte d'aiguillage) qui permettra de suivre
le parcours de l'arbre en fonction de l'état de la condition (vrai ou faux). Ainsi,
pour que la transformation (traduction intermédiaire) soit complète, il faut
générer les deux branches (celle associée à then ainsi que celle associée à else), car
ce n'est qu'à l'exécution que l'on pourra savoir quelle est l'alternative qui est
sélectionnée. La génération des deux branches représentant respectivement les
deux alternatives est donc nécessaire au préalable avant toute traduction cible
finale.
Remarque 2.3
Le but ici, n'est, ni de fournir une description exhaustive et détaillée des
différentes représentations intermédiaires couramment utilisées, ni de se limiter à
une seule d'entre elles. L'intérêt étant tout simplement de montrer comment on
peut passer d'une forme source à une forme intermédiaire équivalente pour les
différentes constructions (affectation, déclaration, etc.) que l'on rencontre
couramment dans les langages de programmation.
Pour simplifier, on utilise une forme intermédiaire qui soit claire et facile à
exploiter lors de la génération du code cible. On opte pour le code à trois adresses
pour plusieurs raisons que l'on explicitera ci-dessous.
T4 := T2 and T3
Remarque 2.4
Les deux champs dédiés aux opérandes ne sont pas forcément toujours remplis
simultanément. En effet, l'un d'eux, peut être libre quand on a à faire à une
opération unaire. Ils peuvent être, tous les deux, vacants quand on a une
opération de branchement ; dans ce cas, c'est le champ dénoté par la variable
temporaire qui peut faire l'objet de l'adresse de branchement.
Donc, parfois le nombre de champs se réduit à trois, voire même à deux, ce qui
implicitement sous-entend une perte de place qu'il faudrait essayer d'éviter en
adoptant une structure de données plus adéquate. En effet, par exemple
l'opération de branchement GOTO Etiquette, ne nécessite que deux champs, l'un
pour l'opérateur de branchement (GOTO) et l'autre pour l'adresse de
branchement (Etiquette). Pour harmoniser l'écriture, il faut fixer au préalable
quels sont les champs qui seront utilisés pour qu'ils soient repérés facilement lors
de la génération du code cible final.
Il existe d'autres variantes pour optimiser le code à trois adresses, on parle
alors de « triplets » qui sont une forme de code à trois adresses réduite,
dépourvue de la zone temporaire. Dans ce cas, le champ habituellement attribué
au temporaire (quatrième champ) n'est pas créé, mais utilisé à la volée
(dynamiquement) quand on référence la séquence de code où il devrait apparaitre
théoriquement. Le code à trois adresses comportant explicitement la variable
temporaire se nomme généralement « quadruplet » en référence au nombre de
champs qui est au plus égal à quatre. Ainsi, on parlera désormais, indifféremment
de code à trois adresses pour désigner « quadruplet » ou « triplet ». Par exemple,
l'expression 11 (a * 2 > c) and ( b < 5) 11 , a pour code à trois adresses, la séquence
de code suivante :
(1) *, . a, 2, Tl
(2) >, Tl, c, T2
(3) <, b, 5, T3
(4) and, T2, T3, T4
si l'on emploie les quadruplets, et la séquence de code suivante :
(1) *, a, 2
(2) >, (1), c
(3) <, b, 5
(4) and, (2), (3)
si l'on adopte les triplets.
Remarque 2.5
Le code à trois adresses est beaucoup plus simple à obtenir pour n'importe quel
type d'instruction, comparativement aux autres formalismes (représentation post-
fixée et arbre abstrait); il suffit de respecter le formalisme de traduction associé
aux instructions.
En effet, comme on a pu le constater, à travers les quelques exemples présentés
ci-dessus. Par exemple, l'arbre abstrait de l'instruction <if_ then_ else> montre
Traduction 301
qu'on est contraint d'ajouter des nœuds artificiels supplémentaires pour exprimer
l'aiguillage de la condition. L'aiguillage permettra, lors de la traduction en code
cible, de connaitre quel est le chemin (en terme de branche d'arbre) qu'il va falloir
emprunter pour traduire complètement l'instruction conditionnelle.
Il faut noter, par ailleurs, que l'instruction conditionnelle <if_ th en_ else>
elle-même, constitue l'ossature de contrôle de base pour les autres instructions de
contrôle comme <while do>, <if_ then>, <repeat_ until>, etc. Ce qui sous-
entend, qu'il y aura autant de nœuds d'aiguillages à ajouter (donc de nœuds
artificiels supplémentaires) qu'en nécessite l'instruction de contrôle concernée.
Plusieurs raisons, dont on citera ci-après les plus importantes, placent le
formalisme du code à trois adresses comme la meilleure option relativement aux
deux autres (forme post-fixée et arbre abstrait). En effet, le code à trois adresses :
Ne nécessite pas de structures de données complexes comme les structures
hiérarchiques suggérées par les arbres abstraits.
C'est une représentation séquentielle qui met en valeur tous les détails
concernant chaque opération (arithmétique ou autre). L'expression " (a * 2 >
c) and (b < 5)" de l'exemple précédent est représentée par une séquence de
quadruplets qui montrent clairement comment se succèdent séquentiellement
les différentes opérations permettant d'exprimer la condition.
Les branchements vers des adresses non encore définies sont gérés et réglés par
des astuces très simples au cours du processus de génération du code en soi.
Ceci dit, mais il n'est pas exclu toutefois, dans le cadre d'un exercice ou d'un
exemple d'application, d'opter pour un autre type de représentation intermédiaire.
Cela permet de mieux expliciter le processus de traduction (passage de la forme
source d'une construction à la forme intermédiaire équivalente). Par ailleurs, cela
rappelle aussi que le code à trois adresses n'est pas toujours la seule option
possible, et qu'il sera fait appel à un autre formalisme en cas de nécessité.
302 Chapitre 7
Remarque 2.6
L'algorithme de parcours en profondeur peut être appliqué pour n'importe quel
modèle de traduction (forme post-fixée, arbre abstrait ou code à trois adresses).
L'algorithme en question a été présenté et expliqué à la fin du chapitre 4.
Expression arithmétique
Soit l'expression "a * (b + c) + b / c". Les variable a, b et c sont de type
arithmétique.
Le code intermédiaire de l'expression est représenté par la séquence de
quadruplets suivante :
(1) +, b, c, Tl
(2) *, a, Tl, T2
(3) /, b, c, T3
(4) +, T2, T3, T4
Remarque 2.7
Il est important de remarquer que la génération des quadruplets ne se fait pas
selon l'ordre d'apparition des opérateurs dans l'expression. En effet, il y a une
priorité dans l'exécution des opérations compte tenu de la précédence (priorité)
des opérateurs. Ceci est entièrement pris en charge dans les différentes actions
sémantiques de traduction associées qui ont pour rôle d'assurer le bon
déroulement de la traduction. Cela se produit généralement parallèlement à
l'analyse syntaxique qui coordonne les routines de traduction.
Expression relationnelle
Une expression relationnelle établit une relation de comparaison ( <, <=, >,
>=,=, -:t:., etc.) entre des expressions arithmétiques. Les variables a, b et c sont de
type arithmétique.
L'expression "a * (b + c) > b " possède pour code intermédiaire, la séquence
de code suivante :
(1) +, b, c, Tl
(2) *, a, Tl, T2
(3) >, T2, b, T3
ci-dessus. Ainsi, pour une expression logique, on aura à générer les quadruplets
correspondants en respectant scrupuleusement l'ordre d'exécution des différents
opérateurs (arithmétiques, relationnels et booléens) de l'expression en question.
Soit alors à construire le code à trois adresses pour l'expression logique
"a* (b + c) > b OR (NOT d OR e)". Les variables a, b et c sont de type
arithmétiques, tandis que d et e sont des variables logiques (booléennes)
Le code intermédiaire de l'expression logique ci-dessus est le suivant :
(1) +, b, c, Tl
(2) *, a, Tl, T2
(3) >, T2, b, T3
(4) NOT, d, T4
(5) OR, T4, e, T5
(6) OR, T3, T5, T6
Remarque 2.8
L'expression logique étant la brique de base permettant de construire une
instruction conditionnelle. Il faut également rappeler que l'instruction
conditionnelle classique <if-then-else> constitue l'instruction pivot permettant
de construire tout type d'instruction de contrôle.
Ici, on est dans le cas de variables simples. On verra par la suite avec les
variables indicées représentant des éléments de vecteur ou de tableau que la
traduction nécessite d'introduire certains quadruplets auxiliaires indispensables
pour le calcul d'adresses des éléments de vecteur ou de tableau (l'adresse de base
Traduction 305
Remarque 2.9
La génération de la forme intermédiaire correspond à l'exécution des routines
sémantiques de traduction parallèlement à l'analyse syntaxique. Mais, pour
simplifier, on ne reviendra pas sur ce qu'on a appelé une DDS (Définition Dirigée
306 Chapitre 7
Remarque 2.10
Le branchement doit être effectué à une adresse qui se trouve dans le champ
correspondant à la variable temporaire du quadruplet courant. Cette adresse n'est
pas toujours connue à l'avance. Il va donc falloir attendre de rencontrer l'adresse
en question (désignée par le numéro d'un quadruplet) pour revenir ensuite
compléter (mettre à jour) le quadruplet incomplet généré auparavant.
La forme de la séquence de code intermédiaire est inspirée de la sémantique de
l'instruction de contrôle à traduire. Ainsi, pour l'instruction conditionnelle
<if-then-else>, on a le modèle de traduction suivant :
(i) BZ, TO, (j)
(ii) Séquence de code pour le bloc Il {lorsque la valeur de TO ':f:. O}
(iii) BR, @dr {quadruplet BR qui vient après avoir terminé le bloc
d'instruction 11}
(iv) Séquence de code pour le bloc 12 {lorsque la valeur de TO = O}
(v)
TO étant la variable temporaire où est recueillie la valeur supposée de
l'expression qui est censée représenter la condition notée "cond".
BZ étant le code opérateur indiquant un branchement à l'adresse (non
encore connue) j lorsque la condition "cond" est égale à faux (BZ : Branch
on Zero). L'adresse j est le premier quadruplet du bloc (iv) du modèle de
Traduction 307
a) * a, b, Tl
b) >, Tl, 2, T2
c) c, d, T3
d) AND, T2, T3, T4
e) BZ, T4, (i)
'
f) +, x, 1, T5
g) ASSIGN, T5, ,x
h) BR, (k)
i) x, 1, T6
j) ASSIGN, T6, X
k)
Cette manière de traduire est correcte mais pas toujours optimale. En effet,
souvent on a à faire à des expressions logiques comme par exemple (a * b > 2)
and ( c = d) qui ne nécessitent pas d'être calculées complètement pour connaitre
le résultat final qu'elles délivrent. Donc, lorsque le résultat de la sous-expression
(a * b > 2) n'est pas vrai, il n'est pas nécessaire de continuer le calcul, puisque
avec l'opérateur and, quelle que soit la quantité (vrai ou faux) fournie par la
sous-expression ( c = d), le résultat final sera de toute façon faux. L'opérateur or
est également concerné par cette question qui sera traitée en détail dans une
partie ultérieure de la présente section.
On voit bien qu'il s'agit d'une forme simplifiée du modèle de traduction associé
à l'instruction conditionnelle <if-then-else>.
BR, @adr représente un branchement inconditionnel vers l'adresse @dr qui est
le numéro du quadruplet de la séquence de quadruplets associée au calcul de la
condition "cond".
Vn-1 : In-1
autre: In
Le modèle de traduction se présente comme suit :
(1) Séquence de code pour l'expression E
(2) Stockage de E dans le temporaire TO
(3) BR, (j) {branchement inconditionnel vers le début du bloc test (11) du
modèle de traduction courant}
(4) Séquence de code pour l'instruction I 1
(5) BR, (s) {branchement inconditionnel en sortie de l'instruction selonque
(15)}
(6) Séquence de code pour l'instruction h
(7) BR, (s) {branchement inconditionnel en sortie de l'instruction selonque
(15)}
(8)
(9) Séquence de code pour In {In ne fait pas partie du lot Il, h, .. .In-1 du bloc
selonque (15)}
312 Chapitre 7
3) ASSIGN, Tl, X
4) BR, (18)
5) +, x, 20, T2
6) ASSIGN, T2, X
7) BR, (18)
8) +, x, 30, T3
9) ASSIGN, T3, X
lO)BR, (18)
11)+, x, 50, T4
12)ASSIGN, T4, X
13)BR, (18)
14)BE, prix, 5, (2)
15)BE, prix, 10, (5)
16)BE, prix, 15, (8)
17)BR, (11)
18) ...
Remarque 2.11
Le modèle de traduction décrit ci-dessus ne donne pas d'information sur la nature
ou la valeur des étiquettes Vj. En effet, l'aiguillage <selonque> présenté ci-dessus
se limite à découper l'instruction en plusieurs branchements selon la valeur de Vi
(i = 1..n) sans pour autant donner de précision sur Vi qui peut concerner toute
une plage (intervalle) de valeurs. Ces dernières elles-mêmes, peuvent être de type
entier ou de type caractère ou autre.
Une autre manière de traduire l'instruction de contrôle à choix multiple ci-
dessus peut être décrite par une série de branchements conditionnels. Chacun des
branchements teste une valeur Vj et transfert le contrôle à l'instruction
correspondante li. Cependant, ceci n'est intéressant que lorsque le nombre de cas
n'est pas trop grand, 10 au plus. Le modèle de traduction est le suivant :
(1) Séquence de code pour l'expression E
(2) Stockage de E dans le temporaire TO
(3) *
BNE, TO, v1, (Ll) {tester si TO v1 alors se brancher à Ll (6)}
(4) Séquence de code pour Il
(5) BR, (j) {branchement inconditionnel vers la sortie de l'instruction
selonque (15)}
(6) *
BNE, TO, 112, (L2) {tester si TO 112 alors se brancher à L2 (9)}
(7) Séquence de code pour 12
(8) BR, (j) {branchement inconditionnel vers la sortie de l'instruction
selonque (15)}
(9)
(10)
(11) BNE, TO, Vn-i. (Ln-1) {tester si TO -::F Vn-1 alors se brancher à Ln-1 (14)}
(12) Séquence de code pour In-1
(13) BR, (j) {branchement inconditionnel vers la sortie de l'instruction
selonque (15)}
(14) Séquence de code pour In
(15)
314 Chapitre 7
A ce stade, comme prévu plus haut, on revient sur l'utilisation optimale des
expressions logiques. En effet, souvent on a à faire à des expressions logiques qui
ne nécessitent pas d'être calculées complètement pour connaitre le résultat final
qu'elles délivrent.
Traduction 315
Codage numérique
Dans ce mode de représentation, on attribue en général 1 pour représenter vrai et
0 pour représenter faux. Tout comme les expressions numériques, l'ordre
d'évaluation de toute expression booléenne est de gauche à droite. A titre
indicatif, le code à trois adresses de l'expression booléenne x OR y AND NOT z
est le suivant :
(1) NOT, z, Tl
(2) AND, y, Tl, T2
(3) OR, x, T2, T3
Ainsi, selon le codage numérique des expressions booléennes, la traduction d'une
expression relationnelle comme x > y est identique à la traduction de l'instruction
conditionnelle si x > y alors 0 sinon 1. Le code à trois adresses de la relation
x > y est alors représenté comme suit :
1) BG, x, y, (4)
2) ASSIGN, 0, Tl
3) BR, (5)
4) ASSIGN, 1, Tl
5)
Rappelons que l'opérateur BG (Branch on Greater than), au lieu de BZ
(Branch on Zero) permet de bénéficier d'une ligne de code de moins comme avec
certains exemples vus plus haut.
On peut également traduire une expression logique sans pour autant générer le
code de tous les opérateurs booléens. Cette option permet l'évaluation des
expressions logiques en calculant uniquement les parties ou les sous-expressions
nécessaires et suffisantes pour déterminer la valeur de l'expression. A titre
d'exemple, le code à trois adresses de l'expression "logique x > y OR z < s
AND r < v 11 est représenté par la séquence de code suivante commençant
arbitrairement au numéro de quadruplet (1) :
1) BG, x, y, (4)
2) ASSIGN, 0, Tl
316 Chapitre 7
3) BR, (5)
4) ASSIGN, 1, Tl
5) BL, z, s, (8)
6) ASSIGN, 0, T2
7) BR, (9)
8) ASSIGN, 1, T2
9) BL, r, v, (12)
lO)ASSIGN, 0, T3
ll)BR, (13)
12)ASSIGN, 1, T3
13)AND, T2, T3, T4
14)0R, Tl, T4, T5
15) ...
Codage positionnel
Le but ici, n'est pas de revenir sur toutes les expressions logiques ou les
instructions conditionnelles étudiées jusque-là, mais de s'appuyer sur un simple
exemple afin d'expliquer le principe du codage positionne!. On se focalisera à
l'occasion aussi sur l'optimisation de la traduction des expressions logiques.
Tel qu'on l'avait indiqué, conformément au principe du codage positionne!,
une expression booléenne est traduite en une séquence de branchements
conditionnels et inconditionnels. Ainsi, Ltrue est l'adresse symbolique à atteindre
lorsque l'expression booléenne est égale à vrai, et Lfalse est l'adresse symbolique à
atteindre lorsque l'expression en question est égale à faux. Si l'on suppose que
l'expression est stockée dans un temporaire T, alors la séquence de code associée
serait de la forme :
(1) BNZ, T, Ltrue
(2) BR, Lfalse
L'opérateur BNZ (Branch on not Zero) est l'inverse de BZ. Autrement dit, le
branchement aura lieu à la position Ltrue qui est l'adresse symbolique à atteindre
quand l'expression recueillie dans T est vrai.
A présent, si l'on suppose que l'expression E est la combinaison El or E2.
Alors, dans ce cas on sait que si El est à vrai alors on est sûr que l'expression E
Traduction 317
est à égale à vrai également. Mais, si El est égale à faux, il va falloir évaluer E2 ;
dans ce cas, E.false est l'étiquette qui référence la première instruction du code de
E2. Ainsi, les sorties vrai et faux de E2 correspondent respectivement aux sorties
vrai et faux de E.
De même, si l'expression E était de la forme El and E2, on aurait tenu un
raisonnement analogue à celui de la traduction de l'expression El or E2. En effet,
si El est égale à faux, alors on sait que E est égale à faux également. Mais, si El
est égale à vrai, E2 doit être évaluée, donc on attribue la valeur de l'étiquette
(E.true) qui référence la première instruction du code de E2. Ainsi, les sorties vrai
et faux de E2 ont les mêmes valeurs, respectivement, que les sorties vrai et faux
de E. Aussi, la sortie faux de El a la même valeur que la sortie faux de E.
Quant à l'expression not E, il suffit de permuter E.true et E.false de E,
respectivement avec E.false et E.true de not E.
On reprend ces idées et on dresse la liste complète des règles sémantiques de
traduction en code à trois adresses des expressions booléennes. On utilise pour
cela la définition dirigée par la syntaxe associée aux règles de production des
expressions booléennes. On reprend le formalisme utilisé pour construire la
définition des règles sémantiques. On rappelle que la concaténation des chaines
dans les règles sémantiques est notée par le symbole Il· Ce dernier a été repris
dans la définition dirigée par la syntaxe ici dans le Tableau LI! :
On précise que Newlabel est une fonction qui crée une nouvelle étiquette comme
El.true, El.false, E.true, etc. Genest une procédure qui produit ou qui génère une
partie du code. Ce dernier peut être une étiquette suivie de deux points ':' ou tout
simplement une séquence de code associée à l'expression E proprement dite.
On reconsidère l'expression booléenne "x > y OR z < s AND r < v" traitée
précédemment avec la méthode du codage numérique, et on lui applique la
traduction basée sur le codage positionnel. On désigne par Etrue et Efalse les
valeurs attribuées respectivement aux sorties vrai et faux de l'expression
considérée.
Remarque 2.12
Jusqu'à présent, on n'a pas encore traité le cas des étiquettes symboliques au sein
des séquences de code, car pour la majorité des cas étudiés on a toujours opté
pour le codage numérique. Les étiquettes symboliques nécessitent d'ajouter un
champ supplémentaire qui précède le 1er champ du code à trois adresses appliqué
jusqu'alors.
Ainsi, conformément à la définition du Tableau LII, on obtient pour
l'expression, "x > y OR z < s AND r < v", la séquence de code suivante :
BG, x, y, Et rue
BR, Ll
Ll : BL, z, s, L2
BR, Efalse
L2: BL, r, v, Etrue
BR, Efalse
Le code généré n'est pas optimal. En effet, on peut remarquer par exemple que la
deuxième instruction BR, Ll est redondante, puisque son élimination n'influe pas
sur l'effet du code. On peut utiliser un optimiseur pour éliminer ce genre de
redondances. Il faut remarquer aussi qu'ici on n'a pas numéroté les séquences de
code. Cela suppose que l'on n'emploiera pas le même générateur de code cible ou
interpréteur que celui qu'on aurait utilisé avec le codage numérique. En somme, il
faut faire un choix entre le codage numérique et le codage positionnel afin de fixer
quel type de générateur de code cible utiliser par la suite.
Enfin, comme mentionné plus haut, on décrira dans ce qui suit, la traduction
des variables indicées et tableaux.
tableau est notée taille, le ieme élément d'un tableau nommé A, se trouve à
l'adresse suivante :
@A [i] = base + (i - inf) * taille
où base est l'adresse relative d'implantation du tableau A ; donc, de son premier
élément. L'indice inf étant la borne inférieure du tableau A. L'élément A[in.fl
correspond donc à base. L'adresse base + (i - inf) * taille peut être connue
partiellement à la compilation si elle est mise sous la forme suivante :
@A [i] = i * taille + base - inf * taille
La sous-expression C = base - inf * taille, nommée partie constante, peut être
évaluée lors de la déclaration du tableau A, et sauvegardée dans la table des
symboles ; il suffit donc d'ajouter i * taille, à C pour obtenir l'adresse relative de
l'élément A[i].
La généralisation à un tableau à deux dimensions nécessite de savoir comment
sont rangés les éléments du tableau (ligne par ligne ou colonne par colonne). Si on
opte pour un rangement ligne par ligne, l'adresse relative d'implantation est
calculée comme suit :
@A [i1, 'Ï2] = base + ((ii - inf1) * 7i2 + 'Ï2 - inh) * taille
Les bornes inférieures et supérieures des indices lignes ( ii) et colonnes ( 'Ï2) sont
désignées par inf1 et in/2, respectivement. Le terme 7i2 représente le nombre
d'éléments dans une colonne ( 'Ï2). Ainsi, si sup2 est la borne supérieure de 'Ï2, alors
7i2 = sup2 - inh + 1. Tout comme avec le tableau à une seule dimension, on peut
reformuler le calcul d'adresse comme suit :
@A [ii, 'Ï2] = ( ii * 7i2 + 'Ï2) * taille + base - ( inf1 * 7i2 + inh) * taille
Ici la partie constante qui peut être évaluée à la compilation est :
C = base - ( inf1 * 7i2 + inh) * taille
Le calcul peut être généralisé à plusieurs dimensions, et après toutes les
transformations, on obtient la formule qui représente l'adresse relative de
l'élément A [i1, 'Ï2, ... , ik] :
@A [ii, 'Ï2, ... , ik] = (( ... (i1 * 7i2 + 'Ï2) * n3 + i3) ... ) * nk + ik) *taille
+ base - ( ... (inf1 * 7i2 + inh) * n3 + inf3) ... ) * nk +
infk) * taille
U1 = ii
Um = Um-1 * 7irn + im
320 Chapitre 7
(1) *, ii, 6, Tl
(2) +, Tl, l2, Tl
(3) ASSIGN, C,, T2 / * Partie constante C = - 27 du vecteur A * /
(4) *, Tl, 4, , T3
(5) TAB, T2, T3, T4
Ainsi, pour générer une forme intermédiaire basée sur le code à trois adresses
(quadruplets), on peut solliciter, par exemple, la procédure dénommée GEN
suivante dans le traducteur :
procédure GEN (op, argl, arg2, T) ;
début
quad [nextquad, l] ~ op ;
quad [nextquad, 2] ~ argl ;
quad [nextquad, 3] ~ arg2 ;
quad [nextquad, 4] ~ T ;
nextquat ~ nextquad + 1
fin;
La variable « quad » est le nom de la table où l'on est supposé stocker les
quadruplets. La variable « nextquad » dans la procédure GEN correspond au
numéro du quadruplet suivant. Cette variable est censée être initialisée à 0 avant
toute action de traduction lorsqu'on utilise lé code à trois adresses.
Traduction 321
Pour clore ce volet on tient à rappeler que tous ces contrôles peuvent être
réalisés en les introduisant, soit dans des DDS (Définition Dirigée par la Syntaxe),
c'est-à-dire des règles sémantiques, soit dans des STDS (Schémas de Traduction
Dirigée par la Syntaxe), c'est-à-dire des actions sémantiques.
A cette issue, on est plus à même de dire qu'un tour d'horizon a été effectué
sur l'essentiel concernant la partie frontale de la traduction, en l'occurrence, la
traduction en code intermédiaire. Ce travail a été essentiellement axé sur des
exemples d'application. Pour rappel, la forme intermédiaire choisie et sur laquelle
on s'est le plus attardé est le code à trois adresses. La partie qui fait suite à la
génération de code intermédiaire est la génération de code objet ou code machine
cible. C'est une partie qui requiert au préalable :
les langages intermédiaires dont les instructions sont, soit interprétées par des
machines virtuelles, soit compilées à nouveau.
Mais, pour rester cohérent vis-à-vis de ce qui a été convenu depuis le début de cet
ouvrage, un langage cible est un langage machine (langage d'assemblage ou
langage machine translatable ou langage machine absolu).
Par ailleurs, on peut dire qu'il existe deux catégories de générateurs de code
cible :
le premier est direct (une seule cible), c'est-à-dire génère directement du code
objet ou cible final sans génération de forme intermédiaire ;
le second est indirect, c'est-à-dire qu'il est basé sur une forme intermédiaire
(post-fixée, arbre abstrait, ou code à trois adresses, etc.) qui permet de cibler
n'importe quelle machine (multi cible).
Si on se place dans le contexte de la deuxième catégorie, un générateur de code
cible a comme entrée une représentation intermédiaire du programme source, et
comme résultat généré en sortie, un programme machine cible équivalent. Ce
dernier doit être dépourvu de tout type d'erreur (lexicale, syntaxique et
sémantique) ; il doit en outre être optimisé pour s'exécuter le plus rapidement
possible.
été effectuée avec succès. Ainsi, le générateur de code cible peut travailler sur
une forme intermédiaire sans erreurs.
Choisir la forme cible finale que l'on voudrait obtenir en sortie. Chaque forme
cible (basée sur le langage d'assemblage, le langage machine translatable ou le
langage machine absolu) possède ses avantages, mais aussi ses inconvénients.
• La génération de la forme basée sur le langage d'assemblage s'avère être
plus simple. En effet, on peut produire des instructions symboliques et
utiliser les facilités permises par les macros de l'assembleur. Néanmoins, un
coût supplémentaire est nécessaire à la phase d'assemblage après la
génération de code. Ce choix semble judicieux puisque la génération de
code assembleur ne répète pas exactement la tâche de l'assembleur.
• Le code basé sur le langage machine translatable, quant à lui, permet la
compilation séparée de sous-programmes. Bien que les phases d'édition de
liens nécessitent un coût supplémentaire, ce choix permet une grande
flexibilité d'utilisation du code.
• Enfin, le code basé sur le langage machine absolu a pour avantage d'être
ramené (chargé) directement dans un emplacement donné de la mémoire,
et immédiatement exécuté.
MOV x, Ri
ADD y, Ri
MOV Ri, z
MOV z, Ri
ADD w, Ri
MOV Ri, t
qui est non optimale, car renfermant l'instruction redondante MOV z, Ri qui
vient immédiatement après l'instruction MOV Ri, z. Ceci vient du fait que la
variable z apparait comme résultat dans MOV Ri, z, alors qu'elle est réutilisée
aussi comme opérande dans MOV z, Ri. Autrement dit, il n'était pas
nécessaire de remettre z dans Ri, car il y était déjà. Il en est de même avec
l'instruction MOV Ri, z, si la variable z ne sera pas réutilisée.
La qualité du code généré dépend de sa vitesse d'exécution et de sa taille. Une
machine dotée d'un jeu d'instructions riche permet plusieurs manières
d'implanter une opération donnée. Ainsi, une traduction naïve (instruction par
instruction) peut fournir du code correct mais pas optimal. Par exemple, si la
machine cible est dotée de l'instruction d'incrémentation (INC), elle permet
d'implanter l'instruction à trois adresses +, x, 1, x en utilisant uniquement
INC, x que d'utiliser la séquence de code suivante :
MOV x, Ri
ADD #1, Ri
MOV RO, X
visuellement distinctes. Quand il n'y a pas de suffixe, cela signifie que l'opération
est de mémoire à registre excepté STM qui est évidemment de registre à mémoire.
Les suffixes sont :
'a' : la partie adresse doit être interprétée comme une adresse ;
'i' : indique un adressage indirect ;
'n' : la partie adresse doit être interprétée comme un nombre ;
'r' : de registre à registre ;
's' : de registre à mémoire.
Par exemple, NEGr signifie qu'une valeur est dans un registre. De même,
ADDr 5, 2, indique une addition entre deux registres, et le résultat est placé dans
le registre approprié (1er opérande de l'instruction selon le format donné ci-
dessus). Pour élucider les cas des codes opération suffixés ci-dessus, on ajoute les
exemples suivants :
ADD 1, 8 / * ajouter le contenu d'adresse mémoire 8 au contenu du
registre 1 et placer le résultat dans le registre 1 * /
ADDr 1, 2 / * ajouter le contenu du registre 2 à celui du registre 1 et
placer le résultat dans le registre 1 * /
ADDs 1, 8 / * ajouter le contenu du registre 1 au contenu de la mémoire
d'adresse 8 et stocker le résultat à l'adresse mémoire 8/
ADDn 1, 2(3) / * ajouter le nombre résultant de l'addition de 2 et du
contenu du registre 3 au contenu du registre 1 et placer le résultat
dans le registre 1 * /
ADDa 1, 2(3) / * ajouter l'adresse formée par la combinaison de 2 et du
contenu du registre 3 au contenu du registre 1 et placer le résultat
dans le registre 1 * /
Enfin, la lettre f (floating-point ou virgule flottante) qui préfixe par exemple
ADD et SUB, pour produire fADD et fSUB, signifie qu'il s'agit respectivement
d'une addition et d'une soustraction de type réel, en virgule flotante. Les
opérations xSUB et xDIV dénotent la permutation ou l'inversion des opérandes.
A présent, on a suffisamment d'information pour proposer quelques exemples
et montrer comment utiliser les instructions assembleur de la machine
hypothétique définie ci-dessus.
v. <Opérateur * >
Exactement comme l'opérateur or, excepté la dernière instruction qui est
remplacée par l'instruction suivante :
MLTr i, i+1
vii. <nombre>
Si la valeur de nombre est courte
Alors Générer LDRn i, <valeur de nombre>
Sinon Générer LDR i, <adresse où se trouve la valeur>
Remarque 3.1
Les routines sémantiques peuvent être appliquées parallèlement à l'analyse
syntaxique.
330 Chapitre 7
Mais, elles peuvent également être appliquées sur la forme intermédiaire obtenue
après l'analyse syntaxique.
Compte tenu des différentes routines sémantiques décrites ci-dessus, l'exemple
de l'instruction conditionnelle, précédent produit le code suivant :
LDR 2, y / * chargement du contenu de y dans le registre 2 * /
LDRn 3, 10 / * chargement de la valeur 10 dans le registre 3 * /
MLTr 2, 3 / * multiplication des contenus des registres 2 et 3 et
chargement du résultat dans le registre 2 * /
LDR 3, X / * chargement du contenu de x dans le registre 3 * /
ADDr 2, 3 / * additionner les contenus du registres 2 et 3, et
charger le résultat dans 2 * /
LDRn 3, 125 / * chargement de la valeur 125 dans le registre 3 * /
LDRn 1, TRUE / * charger la valeur TRUE dans le registre 1 * /
SKPEQr 2, 3 / * sauter la prochaine instruction si les valeurs des
registres 2 et 3 sont égales * /
LDRn 1, FALSE / * charger la valeur F ALSE dans le registre 1 * /
LDR 2, m / * charger le contenu de m dans le registre 2 * /
ORr 1, 2 / * appliquer ORr aux contenus des registres 1 et 2, et
charger le résultat dans le registre 1 * /
JMPF 1, $Ef / * se brancher $Ef si le contenu du registre 1 est faux
*/
<code pour statement>
i+-i-1;
+
/ "*
"
X
y/ 10
OperateurBin (op, n, i)
ExpArithmétique (n.filsgauche, i) ;
ExpArithmétique (n.filsdroit, i + 1) ;
Générer (opJJ'r', i, i + 1) /*changement de l'opérateur op en opr */
Cette procédure génère une opération entre deux opérandes, l'un est dans le
registre i et l'autre dans le registre i + 1. Le résultat est supposé aller dans le
registre i conformément à la définition de la machine hypothétique introduite en
début de la présente section.
ExpArithmétique (n, i)
selonque n est
'+': OperateurBin (ADD, n, i) ;
,_,. OperateurBin (SUB, n, i) ;
'*' . OperateurBin (MLT, n, i) ;
'/': OperateurBin (DIV, n, i) ;
Les deux nouvelles procédures Feuille et TraitFeuille ont été introduites afin de
mieux contrôler le déroulement de la traduction. La procédure ExpArithmtique
demeure inchangée. On peut remarquer que l'inversion de l'opérateur 'op' dans
BinOperateur nécessite au préalable d'inter-changer (permutation circulaire) les
opérandes gauche et droit. Ces permutations permettent de faire participer
rapidement les opérandes qui sont disponibles afin d'accélérer le processus de
génération de code lors du parcours de l'arbre abstrait de l'expression. Cela
permet également d'optimiser l'utilisation des registres. Il faut noter cependant
que l'opération d'inversion n'est pas disponible sur toutes les machines. Par
exemple, la division inversée et la soustraction inversée sont souvent omises.
Conclusion
[Aho, 73] A. Aho and J. Ulmann « The theory of Parsing, Translation and
Compiling » (Vl: Parsing, V2: Compiling), Prentice Hall, 1973.
[Lesk, 75] M.E. Lesk, «Lex - A Lexical Analyzer Generator », Camp. Sei.
Tech. Rep. No. 39 (Murray Hill, New Jersey : Bell Laboratories), 1975.
Chapitre 1
1 Diagramme syntaxique des nombres binaires 10
2 Exemple d'arbre 15
3 Exemple d'arbres de dérivation 16
4 Deux arbres de dérivation distincts pour un même mot 17
5 Ambigüité de l'instruction conditionnelle if 17
6 Arbres syntaxiques distincts pour une même chaine 11 888 11 18
Chapitre 2
7 Arbres syntaxiques des mots 11 1 2 3 4 11 et 11 1 1 0 11 34
8 Représentation graphique d'une transition dans un automate fini 37
9 Représentation graphique de l'état initial d'un automate 37
10 Représentation d'un état final d'un automate 37
11 Etat simultanément initial et final d'un automate 37
12 Diagramme de transition pour L = {O, 1} * 38
13 Diagramme de transition pour les entiers naturels pairs 39
14 Diagramme de transition d'un automate fini 39
15 Diagramme de transition de l'automate de la Figure 14 semi transformé 40
16 Diagramme de transition de la Figure 14 complétement transformé en
automate simple 41
17 Diagramme de transition de L ={{ab, ba} {ab, ba}n 1 n ~ O} 41
18 Diagramme de transition de la Figure 17 transformé 42
19 Diagramme de transition de L ={a {a, b}n 1 n ~ O} 43
20 Diagramme de transition déterministe de L 44
21 Diagramme de transition déterministe de l'automate AT 45
22 Diagramme de transition de l'automate Al 47
23 Diagramme de transition minimisé de l'automate Al 47
24 Diagramme de transition de l'automate A2 47
25 Diagramme de transition minimisé de l'automate A2 49
26 Diagrammes de transition associés respectivement aux expressions E, a, a *
et a+ 50
27 Diagramme de transition de L(G) = {anbm n, m ~ O} 54
28 Diagramme de transition de L(G) = {b, abn 1 n ~ O} 54
29 Diagramme de transition miroir de L(G) = {b, abn 1 n ~ O} 54
30 Diagramme de la Figure 29 transformé (sans e-transitions) 55
31 Diagramme représentant la grammaire Gm des binaires pairs 56
32 Diagramme miroir du diagramme de la Figure 31 56
33 Diagramme sans e-transitions du diagramme de la Figure 32 56
34 Diagramme de transition de l'expression régulière c (a Etl c) * 58
35 Diagrammes des sous-expressions de l'expression c (a Etl c) 59
338 Table des figures
Chapitre 3
36 Diagramme syntaxique des productions A -7 a 1 B 82
37 Diagramme syntaxique des productions A -7 c 1 abBA 82
38 Diagramme syntaxique des productions A -7 abB 1 abBA 82
39 Graphe syntaxique des productions : S -7 B 1 S + B ; B -7 B * C ; 1 C ;
C -7 a I (S) 83
40 Machine abstraite représentant un automate à pile 84
41 Graphiques d'automate fini (sans pile) et d'automate à pile 87
42 Mise en valeur graphique des états (initial et final) d'un automate à pile 88
43 Diagramme de l'automate à pile (en mode pile vide) de L = {an bn 1 n ~ 1} 89
44 Diagramme de l'automate à pile (mode état final) de L = {anbn 1 n ~ 1} 90
45 Automate à pile (mode pile vide) de L = {an bn 1 n ~ 1} 91
46 Automate à pile (mode état final) de L = {an bn 1 n ~ 1} 92
47 Automate à pile étendu de L = {an bn 1 n ~ 1} 97
48 Automate à pile étendu déterministe pour L = {an bn 1 n ~ 1} 100
49 Exemple de réseau d'automates finis 101
50 RAF de la grammaire Z-7aZb 1 ab 101
51 RAF préliminaire des règles Z -7ZC 1 a; C-7BC 1 b ; B-7Ca 103
52 RAF finalisé des règles Z-7ZC 1 a ; C-7BC 1 b ; B-7Ca 103
53 RAF préliminaire associé à la grammaire des expressions arithmétiques 106
54 Simplification des diagrammes associés aux règles M-7+TM 1 et E-7TM 106
55 RAF simplifié des expressions arithmétiques 107
56 Automate à pile déterministe qui reconnait les expressions arithmétiques 109
57 Machine abstraite représentant un transducteur à pile 110
Chapitre 4
58 Schéma simplifié du fonctionnement d'un compilateur 124
59 Arbre abstrait de l'affectation y := a * b + 10 127
60 Codage (1) en langage LaTex d'une formule mathématique et résultat (2)
généré en sortie par le logiciel LaTex après la compilation du code 128
61 Environnement de compilation d'un programme 129
62 Arbre syntaxique pour y := a * b + 10 131
63 Arbre abstrait pour y := a * b + 10 132
64 Compilation d'une instruction d'affectation 135
65 Exemple de structure de données d'implantation de l'arbre abstrait de
l'instruction d'affectation y := a * b + 10 136
66 Différentes phases de compilation d'un programme 140
67 Phases d'un interpréteur 141
68 Diagramme de transition des entiers naturels 144
69 Diagramme de transition des identificateurs de longueur ~ 3 144
70 Diagramme de transition des identificateurs de longueur quelconque 145
71 Arbre syntaxique de l'expression 11 6 - 1 + 7" 146
72 Arbre abstrait de l'expression "6 - 1 + 7" 146
73 Deux arbres syntaxiques distincts pour la même expression 146
74 Parcours en post-ordre de l'arbre abstrait de l'expression "6 + 1 / 7" 148
339
Chapitre 5
84 Schéma simplifiée d'un compilateur multi-passe 159
85 Interaction entre un analyseur lexical et un analyseur syntaxique 159
86 Un buffer d'entrée en deux moitiés 167
87 Automates pour l'analyse lexicale 174
88 Automates résultant pour l'analyse lexicale 175
89 Automate fini pour l'analyse lexicale des mots-clés for et while 175
90 Automate fini pour l'analyse lexicale des identificateurs 176
91 Automate fini pour l'analyse lexicale des constantes entières 176
92 Automate fini pour l'analyse lexicale des opérateurs{+, , *, /} 176
93 Structure de données pour représenter des tables de transition avec
compromis temps-place 180
94 Automate fini des identificateurs et du mot-clé if 181
95 Aperçu de représentation compressée de la table de transition de l'automate
fini de la Figure 94 182
96 Diagrammes de transition respectivement des mots-clés (begin et end), des
constantes entières et des identificateurs 184
97 Diagrammes de transition pour les nombres réels 186
98 Diagrammes de transition pour les nombres réels et entiers 187
99 Diagrammes de transition reconnaissant trois modèles différents 188
100 Automate fini déterministe équivalent à celui de la Figure 96 191
101 Automate fini déterministe minimal équivalent à celui de la Figure 100 192
102 Création d'un analyseur lexical et son incorporation dans un compilateur 193
103 Diagramme de transition de l'automate d'états finis déterministe pour les
expressions régulières a*b et c + 194
104 Création d'un analyseur lexical à l'aide de Lex 197
105 Arbre abstrait décoré de l'expression régulière (alb)*bbalc+ # 203
106 Diagramme de transition équivalent aux transitions du Tableau XXVI 204
107 Diagramme de transition finalisé de l'expression (alb) *bbalc +# 204
108 Modèle de diagramme de transition basé sur la fonction followpos 205
109 Table des symboles basée sur l'accès par arbre binaire ordonné 211
110 Table des symboles basée sur l'accès dispersé ; résolution des collisions
par adressage ouvert et sondage linéaire 213
111 Table des symboles basée sur l'accès dispersé ; résolution des collisions
340 Table des figures
Chapitre 6
118 Aperçu de classification des méthodes d'analyse syntaxiques 224
119 Automate à pile déterministe modèle d'analyseur descendant pour le
langage L(G) = {anObn 1 n ~ O}u{anlb2n 1 n ~ O} 234
120 Relations entre classes de grammaires à contexte libre 235
121 Relations de précédence d'opérateurs 237
122 Arbre d'analyse du mot 11 a 11 par la descente récursive 247
123 Diagramme de transition de l'automate à pile déterministe modèle
d'analyseur qui reconnait les expressions arithmétiques simple
parenthésées 250
124 Graphe biparti exprimant les relations de précédence d'opérateurs de la
grammaire des expressions arithmétiques simple non parenthésées 257
125 Hiérarchie des grammaires LR (LR, SLR, LALR) 264
Chapitre 7
126 Parcours en post-ordre de l'arbre abstrait de l'expression 11 6 +1/ 7 11 292
127 Arbre binaire abstrait de l'expression TlpT2 292
128 Arbre binaire abstrait de l'expression 11 2 * 4 5 / 2 11 292
129 Représentation dynamique de l'arbre de la Figure 128 294
130 Arbre abstrait de l'expression "(a* 2 > c) and (b < 5) 11 298
131 Arbre abstrait de la condition "if (a* b > 2) and (c = d)" 298
132 Arbre abstrait de l'expression m + h * 60 333
Liste des tableaux
Chapitre 1
I Exemple de dérivation indirecte 9
II Stratégie d'analyse descendante 11
III Séquence d'analyse montante de la chaine "a+ a" 14
Chapitre 2
IV Matrice de transition d'un automate fini 36
V Représentation matricielle de l'automate fini A 36
VI Matrice de transition correspondant au diagramme de la Figure 19 43
VII Matrice de transition déterministe du langage L 44
VIII Matrice de transition de l'automate fini AT 44
IX Matrice de transition déterministe de l'automate AT 45
X Matrice de transition déterministe de AT version finalisée 45
Chapitre 3
XI Représentation matricielle d'un automate à pile 87
Chapitre 4
XII Exemple d'un ensemble de paires (lexème, unité lexicale) 131
XIII Table des symboles d'un assembleur contenant les identificateurs x et y 137
XIV Compilation et interprétation : comparaison 142
XV Définition dirigée par la syntaxe pour la traduction des expressions
arithmétiques en leurs correspondantes préfixées 149
XVI Définition dirigée par la syntaxe pour la traduction des expressions
arithmétiques en leurs correspondantes post-fixées 150
XVII Définition dirigée par la syntaxe pour la traduction des expressions
arithmétiques en leurs correspondantes post-fixées 154
XVIII Schéma de traduction dirigée par la syntaxe pour la traduction des
expressions arithmétiques en leurs correspondantes post-fixées 154
Chapitre 5
XIX Lexèmes avec les unités lexicales associées 161
XX Exemples de lexèmes, unités lexicales et modèles informels associés 161
XXI Exemples d'identificateurs 163
XXII Exigences en temps et en place pour reconnaitre des expressions
régulières 180
XXIII Matrice de transition de l'automate fini déterministe 189
XXIV Matrice de transition de l'automate de la Figure 103 194
XXV Expressions régulières de Lex (Flex) 199
XXVI Transitions à partir des positions 1, 2, 3 et 6 exprimées à l'aide des
342 Liste des tableaux
Chapitre 6
XXXI Ensembles Firstl et Followl 228
XXXII Calcul des ensembles Firstl et Followl modifié 229
XXXIII Calcul des ensembles Firstop et Lastop 237
XXXIV Table prédictive de la grammaire LL(l) des expressions arithmétiques
simples parenthésées 248
XXXV Ensembles Firstop et Lastop associés à la grammaire des expressions
arithmétiques simples non parenthésées 254
XXXVI Table des relations de précédence d'opérateurs issue de la grammaire
des expressions arithmétiques simples non parenthésées 254
XXXVII Table des priorités pondérées issues de la table de précédence du
Tableau XXXVI et de l'algorithme de calcul précédent 255
XXXVIII Table des priorités pondérées des parenthèses ouvrante et fermante
issues de la table de précédence de la grammaire des expressions
arithmétiques simples parenthésées et de l'algorithme de calcul
précédent : Approche algorithmique 256
XXXIX Table des priorités pondérées issues de la table de précédence
du Tableau XXXVI et du calcul du chemin le plus long dans
le graphe 258
XL Matrice représentant les relations de précédence des opérateurs de la
grammaire des expressions arithmétiques simples sans parenthèses 258
XLI Table d'analyse LR pour la grammaire S' ~ S (o); S ~ SaSb (l) 1 e (2) 260
XLII Table d'analyse SLR (1) pour la grammaire S'~S(o) ; s~cc( 1 l ;
C~cd 2 l 1 d( 3) 268
XLIII Table d'analyse LR (1) pour la grammaire S'~S(O) ; S~CC(l) ;
C~cC(2) 1 d(3) 268
XLIV Table d'analyse LALR (1) pour la grammaire S'~S(O) ; S CC(l) ;
C~cC(2) 1 d(3) 269
XLV Table d'analyse SLR(l) pour la grammaire représentée par l'ensemble
des règles E'~E (0) ; E~E + E (l) 1 E * E (2l 1 (E) (3) 1 a (4) 274
XLVI Table d'analyse pour la grammaire représentée par l'ensemble des
règles de production P' = {E'~E ; E~E+T (l) 1 T (2) ;
T~ T*F (3 ) IF (4); F~ (E) (s) la (G)} 276
XLVII Prise en compte des symboles de synchronisation dans la table d'analyse
prédictive LL (1) associée à la grammaire représentée par
les règles de P 279
XVIII Table de précédence d'opérateurs des expressions avec les cas d'erreurs 281
XLIX Automate d'états d'entrée dans les expressions arithmétiques 283
343
Chapitre 7
L Représentation statique tabulaire de l'arbre de la Figure 128 293
LI Liste de quelques symboles de branchement 305
LII Définition dirigée par la syntaxe pour la génération
du code à trois adresses des expressions booléennes 317
Index
L'auteur:
Ali AÏT EL HADJ est enseignant-chercheur à l'université Moulaud Mammeri de Tizi-Ouzou. Titulaire
d'un Doctorat ès sciences et d'une HDR, spécialité informatique, il enseigne aux trois niveaux LMD.
www.editions-ellipses.fr