You are on page 1of 353

LANGAGES INFORMATIQUES

Analyse syntaxique et traduction


Outils et techniques
Cours et exercices résolus

Il

Ali AIT EL HADJ


www.bibliomath.com
TECHNOSUP
Les FILIÈRES TECHNOLOGIQUES des ENSEIGNEMENTS SUPÉRIEURS

LANGAGES INFORMATIQUES

Analyse syntaxique et traduction


Outils et techniques

Cours et exercices résolus

Il

Ali AIT EL HADJ


Maître de conférences
Université ®..Uzi:_Ouzou

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

Table des figures 337

Liste des tableaux 341

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

Ce chapitre constitue un rappel de certaines notions fondamentales issues


de la théorie des langages. L'objectif est d'habituer le lecteur aux concepts
utilisés et fixer certaines notations que l'on retrouvera tout au long de cet
ouvrage. Ces notions ne prétendent aucunement remplacer un cours de
théorie des langages. De ce fait, aucune démonstration théorique n'est
rapportée. Néanmoins, ces notions ont le mérite de résumer l'essentiel des
connaissances permettant d'aborder avec sérénité les techniques de
compilation qui feront l'objet de plusieurs autres chapitres de cet ouvrage.

1 Définitions préliminaires

Définition 1.1 (Vocabulaire)


On appelle vocabulaire (ou alphabet) un ensemble fini non vide de symboles
ou de caractères.
Par exemple :
l'ensemble B = {O, 1} représente l'alphabet de base des nombres binaires dans
le système de numération binaire pur.
D = {O, 1, 2, 3, 4, 5, 6, 7, 8, 9} est le vocabulaire de base pour former des
séquences de chiffres représentant des entiers naturels.
V = {a, ... , z, A, ... , Z} est l'alphabet permettant de former des mots des
langues naturelles comme le Français, !'Anglais, !'Espagnol, etc.
R = {I, V, X, L, C, D, M} est le vocabulaire de base du système de
numération utilisé par les Romains de !'Antiquité.
P = V u D est l'alphabet de base des identificateurs dans un langage comme
Fortran, Pascal, etc.

Définition 1.2 (Chaîne)


Une chaîne (ou mot) sur un alphabet A est une séquence finie éventuellement
vide d'éléments de A.
Par exemple, les mots : 11 maison 11 , 11 jardin 11 , 11 classe 11 , 11 ruelle 11 , 11 1200$ 11 ,
11 188E-2 11 , représentent des chaînes de caractères.

Définition 1.3 (Longueur d'une chaîne)


La longueur d'une chaîne x définie sur un alphabet A est le nombre de
symboles qui composent x. Elle est notée habituellement par lxl.
Par exemple, les longueurs des mots 11 maison 11 et 11 1788E-2 11 sont notées
respectivement par lmaisonl = 6 et l1788E-21 = 7.

www.bibliomath.com
4 Chapitre 1

Définition 1.4 (Chaîne vide)


On appelle chaîne vide, une séquence de longueur nulle.
On la note habituellement (en théorie des langages) par le symbole e (epsilon).
La longueur de e est évidemment égale à zéro (lel = 0).
On note v+, l'ensemble de toutes les chaînes construites à partir des
éléments d'un alphabet V donné. On écrit alors y+= V 1 u V 2 ... u vn ... = U Vi.
i2:l

La puissance « i » représente la longueur des mots de l'ensemble Vi. De même, on


désigne par v*, l'ensemble de toutes les chaînes de v+, auquel on ajoute la chaîne
vide, c'est-à-dire que V*= LJ yi U{ê} = LJ yi = yO U V 1 U V 2 ... UVn ...
i2:1 i~O

On a alors les équations v* =y+ u {e} et y+= v*- {e}.


Par exemple :
Soit V1 = {a} un ensemble contenant un seul caractêre. Alors, l'ensemble de
toutes les chaines de longueur 'luelconque formées à base du vocabulaire V i,
°
est représenté par l'ensemble V 1 = V 1 u V 11 ... uV1n... ={a}° u {a} 1 ...
u{at ... = {e, a, aa, aaa, aaaa, ... ,an, ... }= {an 1 n;:: O}. V1 est dit infini
dénombrable (ou infini énumérable), c'est-à-dire que ses éléments peuvent être
listés sans omission ni répétition dans une liste indexée par des entiers. De
maniêre formelle, un ensemble E est dit dénombrable quand il est équipotent à
l'ensemble des entiers naturels N, c'est-à-dire qu'il existe une bijection de N
sur l'ensemble E.

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.

Définition 1.5 (Concaténation)


Etant données deux chaînes v et w éléments de l'ensemble V •. On appelle
concaténation des chaines v et w, la juxtaposition de v et w. On note
habituellement cette opération par vw ou v.w .

L'ensemble V• est le monoïde libre engendré par l'alphabet V. En effet,


l'opération de concaténation étant la loi de composition interne sur V•. Par
conséquent, quels que soient les éléments (chaînes) V et W E v*, leur juxtaposition
donne lieu toujours à un élément (chaîne) interne à l'ensemble v*, c'est-à-dire que
z = v.w E V•. Cette opération est associative, car quelles que soient les chaines
(u, v et w) E V•, on a toujours (u.v).w = u.(v.w) = u.v.w. En outre, l'élément
neutre pour le monoïde V• est la chaîne vide e , puisque quelle que soit la chaine
v E V•, on vérifie toujours la relation ve = ev = v.

La concaténation, comme on peut facilement remarquer, n'est pas


commutative. En effet, en choisissant le contre-exemple u = ab et w = ba, on
vérifie aisément que la chaîne vw ( abba) est différente de la chaîne wv ( baab).

www.bibliomath.com
Rappels sur les langages formels 5

2 Langages et grammaires

Définition 2.1 (Notion de langage)


Un langage est un ensemble de mots construits à base d'un alphabet.
Par exemple, les mots d'une langue naturelle comme le Français, sont
construits à partir de l'alphabet latin. Le langage des identificateurs est construit
à base des caractères alphanumériques (lettres et chiffres).
Un langage est utilisé de deux façons différentes :
En mode générateur (ou locuteur)
Le langage est décrit par une grammaire qui suit des règles bien prec1ses. Le
locuteur est, par exemple, un programmeur qui rédige un programme dans le
langage de son choix.
En mode récepteur (ou auditeur)
La réception sous-entend reconnaissance. Dans un système d'interaction homme-
machine, cette reconnaissance peut être assurée par des machines virtuelles
comme les automates. Un autre exemple est celui d'un compilateur qui peut être
considéré comme l'auditeur qui reçoit un programme écrit dans un langage donné.

Définition 2.2 (Langage)


Etant donné un alphabet de base V. On appelle langage L sur l'alphabet V,
un sous-ensemble de chaînes de V *. Autrement dit, L ç;;; V *.
Par exemple, étant donné l'alphabet V = {O, 1}. Le monoïde V= * {O, 1} *
correspond à l'ensemble de tous les mots formés à partir de l'alphabet de base V.
Les langages suivants sont inclus dans V * :

L1 est le langage formé de toutes les chaînes de V *de longueur 2. Ce langage


est représenté par l'ensemble noté V 2. La puissance 2 indique que la longueur
des chaines est exactement égale à 2. Ainsi, L1 = V 2 = { v E v* 1 lvl = 2} =
{OO, 01, 10, 11}.

L~ est l'ensemble des chaînes de longueur au plus égale à 2. On note L2 par


V . La puissance *2 indique que la longueur des chaines est bornée
2
supérieurement par 2. Ainsi, L2 = v* 2 = {v E v* lvl ~ 2} = {E, 0, 1, OO, 01,
1

10, 11}.

L3 est le langage représentant des no;iibres binaires impairs. L3 = {_{O, 1}*1}.


On voit ici que la combinaison {O, 1} 1 représente les éléments de V auxquels
on concatène 1 par la droite. Autrement dit, le langage L3 est composé de
toutes les chaines binaires se terminant par 1, représentant, de toute évidence,
des nombres binaires impairs.
. *
L4 est le langage représentant des nombres binaires pairs. L4 = {{O, 1} O}. De
même, on voit très bien que les chaines de L4 se terminent par 0, donc
correspondant nécessairement à des nombres binaires pairs.

www.bibliomath.com
6 Chapitre 1

2.1 Opérations sur les langages


Etant donné un alphabet V et soient L1 et L2 deux langages sur V, c'est-à-dire Ll
et L2 sont inclus (ç;;;) dans v*. On définit les opérations sur les langages, comme en
théorie des ensembles, de la manière suivante :
Union LluL2={wE v*lwe LlOUWE L2}
Intersection LlnL2 = {w E v* 1w E Ll et w E L2}
Produit (Concaténation) Ll.L2 = {vw E v* 1 VE Ll et w E L2}
Par exemple, soient V = {a, b}, Ll = {aa, bb, ab, ba} et L2 = {ab, bb, ba}.
On a alors:
LluL2 = {aa, ab, ba, bb} = Ll
LlnL2 = {ab, ba, bb} = L2
L1.L2 = {aaab, aabb, aaba, bbab, bbbb, bbba, abab, abbb, abba, baab, babb,
baba}.

Itération et étoile d'un langage


• On définit l'itération par la relation Li= L.Li-l avec i;::: 1. Par convention,
L0 = {e}, à ne pas confondre avec 0 qui est le langage vide, ne contenant
aucune chaine, pas même la chaine vide e.
• L'étoile d'un langage est définie par L* = U Li, L* = L0 u L1 +... = U Li
i~O i~O

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:

L1R = {aa, bb, ba, ab}= Ll / * langage palindrome * /


L2R= {ba, bb, ab}= L2 / * langage palindrome * /
Lic = V * - Ll = {e, a, b, {a, b} n J n ;::: 3}
L2c = v* - L2 = {e, a, b, aa, {a, bf 1 n;::: 3}
Ll - L2 = {aa}
( L1 - L2) *= {aa} * = {e, aa ... (aa) n... } = {(aa) n n;::: O} = {a2n n;::: O}.
J J

www.bibliomath.com
Rappels sur les langages formels 7

Définition 2.3 (Grammaire formelle)


On appelle grammaire formelle, le quadruplet noté G = (VN, VT, S, P) où:
VN est un vocabulaire (ou alphabet) non-terminal (on dit aussi auxiliaire), qui
est un ensemble fini non vide (:;t0) de symboles non-terminaux.
VT est un vocabulaire terminal (ou alphabet de base), qui consiste en un
ensemble fini non vide (:;t0) de symboles terminaux.
On note que l'intersection de VT et VN est toujours vide (VTnVT = 0).
S E VN est le symbole initial ou l'axiome de la grammaire G.
P est l'ensemble des règles de production de G, défini par {a~ ~} avec a E
v+ et ~ E v* tel que V = VTUVN. La flèche au niveau de la règle a ~ ~
indique que le membre gauche a produit ( ~) le membre droit ~·

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.

2.2 Classification des grammaires


Chomsky a introduit en 1956 une hiérarchie dans laquelle les grammaires sont
classées en 4 types qui sont alors transmis au type du langage. Ainsi, un langage
de type i est engendré par une grammaire de type i. Les grammaires peuvent être
classées en fonction de la nature de leurs règles de production. Les quatre types
sont hiérarchiquement imbriqués (type 3 c type 2 c type 1 c type 0) de sorte
que les langages de type 0 (dits généraux) incluent les langages de type 1 (dits
contextuels) qui incluent eux-mêmes les langages de type 2 (dits hors-
contexte) qui, à leur tour, incluent les langages de type 3 (dits réguliers). Les 4
types sont définis en fonction de la nature des règles de production. En effet, une
grammaire définie par le quadruplet G = (VN, VT, S, P) sera dite de :

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 2: Si toutes ses règles de production sont de la forme A ~ a avec A E


VN et a E (VTuVN)*.

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

Type 0 : Si ses productions ne sont l'objet d'aucune restriction, c'est-à-dire


a ~ p avec a et p E v* si et seulement si lal ~ 1. Autrement dit, il ne
pourrait exister de règle de la forme E ~ p.
Par exemple, les règles de production suivantes :
s~os lS 1 0 1 1, définissent une grammaire de type 3 (régulière à droite).
1

S~SO Sl 1 0 l 1, définissent une grammaire de type 3 (régulière à gauche).


1

S~S+A 1 A ; A~(S) 1 a, définissent une grammaire de type 2 (hors contexte).


S~CSA 1 CDc, cA~Bc, B~A, D~b, bA~b De, C~a, définissent une
grammaire de type 1 (contextuelle).
S~RT 1 E, R~aRA 1 bRB 1 E, AT~aT, BT~bT, Ba~aB, Bb~bB, Aa~aA,
Ab~bA, aT~a, bT~b, définissent une grammaire de type 0 (générale).

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.

Définition 2.4 (Dérivation)


Soit G = (VN, VT, P, S) une grammaire formelle.
Dérivation directe
On appelle dérivation directe (a, p), et on la note par a=> p. On dit aussi
que p dérive directement de a si 3 la production a ~ p E P.
Dérivation indirecte
On appelle dérivation indirecte, et on la note par a =>* p ou a =>+p. On dit
aussi que p dérive indirectement de a si 3 une succession de dérivations
directes entre a et p. Autrement dit, a=> P1=> P2 => p3 =>Pi=> ... => p.
Avec =>+, on a un nombre strictement positif de dérivations directes (itération
positive) permettant de passer de a à p (a=> Pl => p2 => ... pi => ... => p). En
revanche, avec =>*, on a un nombre positif ou nul de dérivations directes
(transition positive et réflexive). Autrement dit, outre la succession de dérivations
directes, il peut y avoir aussi la dérivation a = p. En bref, ces deux dérivations
(=>+ et => *) sont liées par les deux relations suivantes :
a=>* P =(a=>+ P) v (a= p).
a=>+ P =(a=>* P) A (a* p).
Remarque 2.3
Par abus de langage, on entend souvent par dérivation, une dérivation indirecte.
Par exemple, on donne la grammaire G = (VN, VT, S, P) où:
VN = {S}
VT = {O, 1}
P = {S ~ OS 1 lS 1 0 1 1}
S est l'axiome, et il est l'unique non-terminal de VN.

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.

dérivation en cours règle appliquée chaine obtenue


S => lS S ~ lS lS
s =>lS => lOS s ~os lOS
s =>lS => lOS => lOOS s ~os lOOS
s =>lS => lOS => lOOS => lOOlS S ~ lS lOOlS
s =>lS => lOS => lOOS => lOOlS => 10011 S~l 10011

Tableau 1- Exemple de dérivation indirecte

2.3 Représentation de langages


Très souvent, on utilise les grammaires et les automates pour représenter ou
décrire des langages. Mais, il existe d'autres modèles tout aussi puissants, voire
parfois plus adaptés en pratique, comme les graphes syntaxiques, les expressions
régulières, les diagrammes syntaxiques ou diagrammes de transition, etc., qui sont
des variantes de ces deux systèmes. Formellement, il existe une distinction nette
entre les grammaires, considérées comme des systèmes générateurs, et leurs
homologues les automates, considérés comme des reconnaisseurs. Cependant, si
des systèmes distincts, décrivent le même langage, ils seront considérés comme
équivalents, même s'ils sont spécifiés par des formalismes différents.
Par exemple, on reconsidère la grammaire du paragraphe précédent,
G = (VN, VT, S, P) où :
VN = {S}
VT = {0, 1}
P = {s ~os 11s 1o11}
S est l'axiome, et il est l'unique non-terminal dans VN.
Sur la Figure 1 on présente le diagramme syntaxique équivalent à G, c'est-à-
dire qui décrit le même langage que G, à savoir, le langage des nombres binaires
dans le système de numération binaire pur.
En effet, à l'entrée IN du diagramme, la flèche permet de transiter, soit vers le
cercle qui contient le 0, soit vers le cercle qui contient le 1. On peut sortir

www.bibliomath.com
10 Chapitre 1

immédiatement par OUT évidemment, et là, on a reconnu un 0 ou un 1 ; ce qui


correspond respectivement aux productions S ~ 0 et S ~ 1. Sinon, l'autre flèche
(en pointillés) au niveau de l'entrée IN du diagramme, permet de repartir à
nouveau, soit avec un 0, soit avec un 1, et ainsi de suite ; ce qui correspond
exactement à l'effet des règles de production S ~OS l lS.
En somme, cela signifie que le diagramme est susceptible de reconnaitre un
nombre potentiellement infini de 0 et de 1 (l'ordre importe peu), c'est-à-dire qu'il
accepte les chaines représentant des nombres binaires purs, tout comme la
grammaire G. Ceci démontre intuitivement l'équivalence des deux systèmes, à
savoir, le diagramme syntaxique de la Figure 1 et la grammaire G.

:-----~i
1 N: n_r
G)
:OUT

Figure 1 : Diagramme syntaxique des nombres binaires

Définition 2.5 (Langage engendré par une grammaire)


On appelle langage engendré par la grammaire G = (VN, VT, P, S), l'ensemble
nommé L (G) = {ro E vT• 1 S =>* ro}.
Soit une grammaire G = (VN, VT, P, S). Pour vérifier si une certaine chaîne ro
appartient au langage L (G), on peut adopter, soit une stratégie d'analyse
descendante, soit une stratégie d'analyse ascendante.
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 ro.
Avec l'analyse ascendante, on démarre avec le mot ro et on tente de remonter
vers l'axiome S par une succession de dérivations inverses nommées réductions.
Ces deux stratégies seront étudiées en détails dans le chapitre 6 qui est consacré
aux méthodes d'analyse syntaxique.
Par exemple, on considère la grammaire G = (VN, VT, P, E) avec :
VN= {E, T, F}
VT = {a, +, * , (, )}
P = {E ~ T + E ;E ~ T ;T ~ F * T ; T~ F ; F ~ a;F ~ (E) }
Soit alors à vérifier si la chaine "a * (a + a)" appartient à L(G), en utilisant la
stratégie d'analyse descendante. La dérivation à partir de l'axiome est illustrée
dans le Tableau Il.
La chaîne donnée "a* (a+ a)" est engendrée par la grammaire G. Autrement dit,
la chaine "a * (a + a)" E L(G) puisque on a: E ::::>* a * (a + a). On note que
L(G) est le langage des expressions arithmétiques très simplifié.

www.bibliomath.com
Rappels sur les langages formels 11

La stratégie montante (analyse ascendante) qui utilise des réductions, ne sera


pas présentée à ce niveau, à cause de certaines conditions qui ne sont pas toutes
réunies. Elle sera présentée dans les prochains paragraphes (voir Tableau III).

Définition 2.6 (Dérivation gauche et dérivation droite)


Dérivation gauche (Left derivation)
On dit qu'une dérivation est à gauche, lorsque la succession de ses
dérivations s'effectue à partir de la gauche.

règle appliquée chaine obtenue


E~T T
T ~ F *T F*T
F~a a* T
T~F a*F
F ~ (E) a* (E)
E~T+E a* (T + E)
T~F a* (F + E)
F~a a* (a+ E)
E~T a* (a+ T)
T~F a * (a+ F)
F~a a* (a+ a)

Tableau II- Stratégie d'analyse descendante

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

Définition 2.7 (Dérivations canoniques)


Soit une grammaire G = (VN, VT, P, E) dont les règles de production sont
numérotées. Donc, les règles peuvent être identifiées, chacune par son numéro.
Ainsi, on appelle dérivation canonique de la chaîne co, la trace (suite de
numéros) des règles utilisées pour dériver le mot co, à partir de l'axiome de
G. On note la dérivation canonique par le symbole 1t. A chaque dérivation
gauche (droite) est associée sa dérivation canonique gauche (droite). On
appelle alors :
Dérivation canonique gauche, et on la note 1t1, la trace des règles utilisées
au cours de la dérivation gauche.
Dérivation canonique droite, et on la note 1tr, la trace des règles utilisées
durant la dérivation droite.
Par exemple, on reprend la grammaire précédente et on numérote ses règles
comme suit:
E~T +E (1)
E~T (2)
T ~ F *T (3)
T~F (4)
F ~a (5)
F ~ (E) (6)
La dérivation canonique gauche est obtenue par le biais de la séquence
de dérivations à gauche comme suit :
E::::>T+E délivre le numéro 1
T+E=>F+E délivre le numéro 4
F+E=>a+E délivre le numéro 5
a+E=>a+T délivre le numéro 2
a+T=>a+F délivre le numéro 4
a+F=>a+a délivre le numéro 5
Par conséquent, la dérivation canonique gauche est 1t1 = 1 4 5 2 4 5.
De même, la dérivation canonique droite est obtenue de la manière suivante :
E::::>T+E délivre le numéro 1
T+E=>T+T délivre le numéro 2
T+T=>T+F délivre le numéro 4
T+F=>T+a délivre le numéro 5
T+a=>F+a délivre le numéro 4
F+a=>a+a délivre le numéro 5
Donc, la dérivation canonique droite est 1tr = 1 2 4 5 4 5
Remarque 2.4
On voit bien que 1t1 -:t- 1tr, mais cela n'implique pas que 1t1 est l'inverse (image
miroir) de Jtr. Même si l'inverse d'une dérivation directe est effectivement une
réduction (application d'une règle au sens inverse, c'est-à-dire, au lieu d'appliquer
A~ a., on applique a.~ A, qui signifie que a. se réduit en A), cela n'implique pas

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

Tableau III- Séquence d'analyse montante de la chaine "a+ a"


La configuration (Stop, E, E) indique que la chaîne a été effectivement réduite
à l'axiome E avec succès. C'est la configuration d'acceptation de la chaine "a + a"
présentée en entrée avec la stratégie d'analyse montante. En effet, en récapitulant
les numéros des règles appliquées dans l'ordre de leur utilisation sur la chaine
11 a + a 11 , on a la trace 5 4 5 4 2 1. En inversant cette dernière, on obtient

1 2 4 5 4 5 qui correspond exactement à la dérivation canonique droite


1tr = 1 2 4 5 4 5 obtenue précédemment avec la séquence de dérivations droites
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

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.

Définition 2.8 (Arbre)


Un arbre est un graphe orienté sans circuit tel que :
Il possède un nœud (nommé racine) et un seul où il n'arrive aucun arc.
Il arrive un arc et un seul en tout autre nœud de l'arbre
Les nœuds sans descendants sont appelés feuilles.
Par exemple, sur l'arbre de la Figure 2, les nœuds sont marqués par les
étiquettes r, c, b, a et d. On dit que l'arbre est étiqueté.

r <E - - - - - - - - Racine
Nœud A
interne--------/->\"'-. b _-~'::
_ _ ; .. Feuilles
a d ./.:"''

Figure 2: Exemple d'arbre

Définition 2.9 (Arbre ordonné)


Un arbre est dit ordonné si les fils x1, x2, ... , Xn de chacun de ses nœuds sont
ordonnés de gauche à droite tels que x1 -< x2, ... , -< Xn- La relation -< définit
un ordre d'apparition et non un ordre de tri {alphabétique ou numérique).
Dans la séquence de nœuds frères x 1, x2, ... , Xn, chaque nœud Xi apparait
avant le nœud Xi+1, pour i =1 à n-1.

Définition 2.10 (Arbre de dérivation)


Un arbre de dérivation pour une grammaire hors contexte (ou à contexte
libre} G = (VN, VT, P, S), est un arbre étiqueté et ordonné dans lequel chaque
nœud est étiqueté par un symbole de l'ensemble VTUVN u{e}. Si un nœud
interne a pour étiquette A et si ses descendants directs (fils) sont étiquetés
par Xi, X2 1 ... , Xn, alors A~ X1X2 ... Xn est une règle de production de P.
Les fils (X1, X2,... , Xn) de A sont disposés de gauche à droite selon leur ordre
d'apparition dans la règle de production A~ X1X2 .. .Xn.
Par exemple, étant donnés une grammaire G = (VN, VT, P, S) et un mot co à
analyser. L'arbre syntaxique (ou arbre de dérivation) du mot co avec G correspond
à l'arbre généré lors de la dérivation du mot co à partir de l'axiome S.
Sa racine est étiquetée par l'axiome.
Ses feuilles sont étiquetées par des éléments de VT u {e}.
Ses nœuds internes sont étiquetés par des éléments de VN.
Par exemple, on considère la grammaire G = (VN, VT, P, S) définie par :
VN = {S}

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

Figure 3: Exemple d'arbres de dérivation

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.

Définition 2.11 (Ambiguïté)


Une grammaire G = (VN, VT, P, S) est ambiguë s'il existe un mot co E L(G)
qui possède deux ou plusieurs arbres de dérivation (arbres syntaxiques)
distincts. Autrement dit, G est ambiguë s'il existe un mot co E L(G) avec deux
ou plusieurs dérivations gauches (ou droites) distinctes.
Remarque 2.6
L'existence d'une grammaire non ambiguë pour un langage donné n'est pas une
propriété décidable. Cependant, dans les cas les plus fréquents, on sait éliminer
assez facilement l'ambiguïté.
En effet, il est possible de transformer la grammaire ambiguë précédente
{S ~ S + S 1 S * S 1 (S) 1 a} en imposant une précédence (priorité) et une
associativité des opérateurs comme c'est le cas avec la grammaire {S ~ T + S 1
T; T ~ F * T 1 F; F ~ (S) 1 a} qui engendre le même langage.
Un autre exemple bien connu de grammaire ambigüe que l'on rencontre
souvent avec les langages de programmation, est celui de l'instruction

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.

Figure 4 : Deux arbres de dérivation distincts pour un même mot

(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

Figure 5 : Ambigüité de l'instruction conditionnelle if

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

D'autres exemples, comme S ~ SaS ; S ~ aS 1 S~ ; S ~ aS 1 aS~S et bien


d'autres, conduisent à des grammaires ambiguës. Par exemple, avec la paire de
règles de production s ~ as 1 s~ on a deux dérivations gauches distinctes :
s ::::? as ::::? as~ et s ::::? s~ ::::? as~ qui indiquent qu'il s'agit d'une grammaire
ambiguë.
s
/\
s /s'\
s s

Figure 6 : Arbres syntaxiques distincts pour une même chaine 11 SSS 11

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.

Par exemple, le langage L = {ai bi c1 1 i = j ou j = l} est un langage


intrinsèquement ambigu. Intuitivement, la raison en est que les mots, lorsque i =
j, doivent être générés par un ensemble de règles, différent de celui générant les
mots avec j = 1. Mais, parmi ces mots, il y a ceux, lorsque i = j = 1, qui peuvent
être générés par des mécanismes différents, c'est-à-dire des arbres de dérivation
distincts.

3 Transformations des grammaires hors contexte


Il est souvent nécessaire de transformer une grammaire de manière a imposer
certaines restrictions sur le langage généré. En considérant la grammaire G dont
les règles sont S ~ S + T 1 T ; T ~ T * F 1 F ; F ~ (S) 1 a, on sait qu'elle
engendre le même langage que la grammaire G8 dont les règles
sont S ~ S + S 1 S * S 1 {S) 1 a. Mais, on sait aussi que Gs possède des
caractéristiques indésirables. Tout d'abord, Gs est ambigüe à cause des deux règles
S ~ S + S 1 S * S. Cette ambiguïté peut être éliminée en utilisant la grammaire
Gn avec les règles S ~ S + T 1 S * T 1 T ; T ~ {S) 1 a. L'autre inconvénient de
G, que l'on retrouve encore même dans Gn, concerne la priorité des opérateurs
« + » et « * ». En effet, les expressions 11 a + a * a" et 11 a * a + a", sont
interprétées respectivement comme les expressions "(a+ a)* a" et "(a* a)+ a".
En poussant un peu plus les transformations, on obtient G où les opérateurs « * »
et « + » sont tels que la priorité de « * » est supérieure à celle de « + », et tous
les deux opérateurs sont associatifs à gauche. Autrement dit, si on a 11 a + a + a 11 ,
c'est comme si on avait "(a+ a)+ a", et si on a "a+ a* a", c'est comme si on
avait "a+ (a* a)".
Dans certains cas, une grammaire peut contenir des symboles et/ou des règles
inutiles. Par exemple, dans la grammaire G = {{S, A, B}, {O, 1}, P, S) avec

www.bibliomath.com
Rappels sur les langages formels 19

P = {S ~ OSB 1 01 ; A~ 1}, les symboles A et B sont inutiles. Il en est de même


pour la règle S ~ OSB, puisque le symbole B n'apparait pas à gauche des autres
règles, et la règle S ~ OSB ne mène nulle part. Le symbole A est inaccessible, car
il n'a aucun lien avec les autres règles. D'une manière générale, un symbole, qu'il
soit inaccessible comme A, ou superflu comme B, il sera dit symbole inutile. En
bref, on définit un symbole inutile de la manière suivante :

Définition 3.1 (Symbole inutile)


Un symbole X E VTUVN est dit inutile dans une grammaire hors contexte
G = (VN, VT, P, S) s'il n'existe aucune dérivation de la forme
S ::::> * <XX~ ::::> * a.w~ sachant que a., ~ et w EV T *.
Un autre type de règles superflues peut exister même si la grammaire ne
contient pas de symboles inutiles. En effet, la grammaire S ~ S + T 1 T ;
T ~ T * F 1 F; F ~ (S) 1 a, possède deux règles superflues S ~Tet T ~ F. On
est alors contraint de transiter par le chemin « S ::::> T ::::> F ::::> a » pour dériver le
mot "a". On élimine les règles superflues par substitution au niveau des règles
concernées. On obtient alors une nouvelle grammaire avec les règles suivantes :
S~S +T 1 T *F 1 (S) 1 a ; T ~ T *F 1 (S) 1 a ; F ~ (S) 1 a.

Définition 3.2 (Langage non vide)


Le langage L(G) engendré par G = (VN, VT, P, S) est non vide s'il existe au
moins une dérivation de la forme S ::::> * a. avec a. E V T *.
Le langage engendré par G = ({S, A, B}, {O, 1}, P = {S ~ OSB; A~ 1}, S)
est vide (L(G) = 0) puisque il n'existe aucune dérivation du type S ::::> * a. avec a.
appartenant à vT* = {O, 1}*. En effet, en appliquant la règle S ~ OSB, on boucle
indéfiniment inutilement sans jamais obtenir une chaine terminale (E VT *). La
dérivation à partir de l'axiome S ::::> OSB ::::> OOSBB ::::> ... ::::> OiSBi ... i ~ 1, n'aboutit
à rien puisque S et B sont tous deux des symboles inutiles. Le symbole A est
également un symbole inutile puisqu'il est isolé du reste et n'a, de surcroit, aucun
lien avec l'axiome S.
Le langage engendré par G = ({S}, {O, 1}, P = {S ~ 081 1 e}, S) n'est pas
vide (L(G) *0). En effet, en appliquant les règles de G, on obtient
L(G) = {aibi 1 i ~ O}.

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.

Définition 3.3 (Grammaire e-libre)


On dit que G = (VN, VT, P, S) este-libre si,
soit P ne contient aucune règle de la forme A ~ e,
soit il y a exactement une seule e-production (S ~ e ), et S ne doit pas
apparaitre dans les membres droits des autres règles.

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.

Définition 3.4 (Grammaire cyclique)


Une grammaire G = (VN, VT, P, S) est dite cyclique si elle admet au moins
une dérivation de la forme A::::>+ A, V A E VN.
Les grammaires cycliques peuvent engendrer des boucles infinies au cours
d'une analyse. En effet, soit la grammaire définie par G = (VN, VT, P, S) où
VN= {S, A}
VT= {a, b}
p = {S ~ as 1 A 1 b ; A ~ s 1 bA 1 a}

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.

Le symbole A, à son tour, sera utilisé avec sa première alternative qui


correspond à la règle A ~ S ; et c'est à ce niveau qu'apparait la boucle infinie
puisqu'il y aura également S ~ aS qui ne donnera pas satisfaction, et ainsi de
suite, indéfiniment sans jamais pouvoir s'arrêter.
On rencontre le même problème avec l'analyse montante.
En effet, en reconnaissant le caractère "b", on l'empile.
On réduit ensuite le caractère "b" empilé 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 en
appliquant la règle A ~ S. De même, en ayant A dans la pile, il est réduit en
S, conformément à la règle S ~ A, et ainsi de suite, sans jamais pouvoir
achever le processus d'analyse.

Finalement, on a constaté, aussi bien avec la stratégie descendante, qu'avec la


stratégie ascendante, que le cycle est incontournable. La seule et unique solution
est de s'en débarrasser d'emblée au niveau de la grammaire. Les règles de la
grammaire cyclique précédente S ~ aS 1 A 1 b ; A ~ S 1 bA 1 a, peuvent être
facilement ramenées à celles d'une grammaire acyclique, il suffit de remplacer par
exemple A par S. On aura ainsi :
S ~ aS 1 bS 1 b 1 a, qui est acyclique et qui engendre, évidemment, le même
langage.

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.

Définition 3.5 (Grammaire réduite ou propre)


Une grammaire est dite réduite (ou propre) si elle est :
acyclique
ë-libre
sans symboles inutiles.
Les grammaires définies respectivement par les ensembles des règles
{S ~ aS 1 bS 1 b 1 a} et {S ~ S + T 1 T ; T ~ T * F 1 F ; F ~ (S) 1 a}, sont
réduites, puisqu'elles respectent les trois conditions citées, à savoir, elles sont
acycliques, ë-libres et sans symboles inutiles.

www.bibliomath.com
22 Chapitre 1

Définition 3.6 (Grammaire d'opérateurs)


Une grammaire est dite d'opérateurs si elle est
réduite
n'admet aucune règle de la forme A~ a.BC~ avec A, B, CE VN et a., ~ E
v*, sachant que V = VTuVN. Autrement dit, pas de membre droit avec
deux non-terminaux adjacents.
La grammaire définie par {S ~ S + T 1 T; T ~ T * F 1 F; F ~ (S) 1 a} est
une grammaire d'opérateurs, car elle ne possède aucune règle ayant deux non-
terminaux contigus ou adjacents.

Définition 3.7 (Forme normale de Chomsky)


Une grammaire est sous forme normale de Chomsky (FNC) si ses
productions sont de la forme :
A~ BC, ou
A~a
Si L(G) contient e, alors S ~ e, et S ne doit apparaitre dans le membre
droit d'aucune autre production.
Toute grammaire hors contexte peut être ramenée à la forme normale de
Chomsky.
Soit alors la grammaire représentée par {S ~ aS 1 bS 1 b 1 a}. On peut la
ramener facilement à une grammaire sous FNC équivalente. Il suffit d'introduire
un nouveau symbole A pour avoir S ~AS tel que A~ a 1 b. En récapitulant, on
trouve les règles S ~ AS 1 a 1 b ; A ~ a 1 b, qui sont effectivement sous FNC.

Définition 3.8 (Forme normale de Greibach)


Une grammaire est sous forme normale de Greibach (FNG) si ses
productions sont de la forme A ~ aa. avec a E VT et a. E VN *. Toute
grammaire hors contexte peut être ramenée à une grammaire sous FNG.
Par exemple, la grammaire dont les règles sont {S ~ aS 1 bS 1 b 1 a} est déjà
sous la forme normale de Greibach (FNG).
La grammaire représentée par {S ~ aAb 1 ab ; A ~ aAb 1 ab} n'est pas sous
FNG, mais on peut la ramener facilement à cette forme en introduisant de
nouveaux symboles auxiliaires. D'après ces règles, il n'y a que le symbole 11 b 11 ,
dans les membres droits, qui doit être remplacé par un symbole auxiliaire que l'on
désignera par B. On aura donc, la grammaire sous la FNG équivalente avec les
règles S ~ aAB 1 aB; A~ aAB 1 aB; B ~ b.
Les formes normales de Chomsky et de Greibach peuvent être utilisées par des
analyseurs spéciaux. Ces derniers seront étudiés dans la partie réservée à l'étude
des analyseurs syntaxiques déterministes (voir chapitre 6).
On laissera, à titre d'exercice d'application, le soin au lecteur de procéder lui-
même à la transformation de la grammaire des expressions arithmétiques
représentée par {S ~ S + T 1 T; T ~ T * F 1 F; F ~ (S) 1 a} sous:
la forme normale de Chomsky (FNC).
la forme normale de Greibach (FNG).
www.bibliomath.com
Rappels sur les langages formels 23

Une autre caractéristique indésirable des grammaires hors contexte est la


récursivité à gauche.

On verra que la récursivité à gauche constitue un handicap majeur pour les


analyseurs syntaxiques basés sur la stratégie d'analyse descendante.

Définition 3. 9 (Grammaire récursive)


Une grammaire G = (VN, VT, P, S) est récursive si elle admet au moins une
dérivation de la forme A => + aA~.
Si a = E alors G sera dite récursive à gauche (c'est le cas qui pose
problème).
Si a * E alors G sera dite, soit tout simplement récursive (~ * e), soit
récursive à droite ou récursive terminale (~ = E).
On peut voir cette récursivité comme celle qu'on a l'habitude de rencontrer en
programmation. En effet, si l'on considère la grammaire G = (VN, VT, P, S) où :
VN = {S}
VT ={a, (, )}
P = {S ~ (S) 1 a}

L'utilisation de la règle S ~ (S), où S est dit auto-imbriquant, est considérée


comme un appel récursif non-terminal d'un programme à lui-même. Dans un tel
cas, la récursivité est gérable par pile dans les systèmes de programmation. La pile
est gérée, soit automatiquement par le système de programmation lui-même, soit
par l'utilisateur, tout dépend du système utilisé. On retrouve la faculté de gérer
facilement ce type de récursivité dans les automates à pile qui sont considérés
comme des systèmes équivalents aux grammaires à contexte libre.
Dans le cas où la récursivité est à gauche, la gestion par pile ne résout rien. En
effet, si on a une grammaire G avec des règles comme :
S~ S+ T 1 T ;T ~ T *F 1 F ; F ~ (S) 1 a,
la récursivité à gauche se traduit par une boucle infinie. Par exemple, si on
utilise les productions selon l'ordre de leur apparition dans G, on aura :
S => S + T => S + T + T => S + T + T + T ... => + S { +T} +
qui ne s'arrête jamais, et produit toujours une sous-chaine ayant pour préfixe S. Si
l'on considère que S est une procédure basée sur une règle comme S ~ S + T, elle
effectuera des appels récursifs inutiles et sans retour, puisque lorsque la procédure
S, s'auto-appelle, rien ne se produit, qu'un nouvel appel est déjà lancé.
L'idée est donc, de se débarrasser de cette récursivité. Il faut noter néanmoins,
que la récursivité à gauche n'est gênante que dans le cas de la dérivation gauche,
c'est-à-dire, dans le cas de l'analyse descendante. On verra plus loin dans le
chapitre 6, que la stratégie d'analyse syntaxique ascendante ne nécessite pas de
supprimer la récursivité à gauche. Autrement dit, les méthodes basées sur la
stratégie montante s'avèrent plus générales et moins contraignantes.

www.bibliomath.com
24 Chapitre 1

3.1 Algorithme de suppression de la récursivité gauche


Entrée: Grammaire hors contexte réduite G = (VN, VT, P, S).
Sortie: Grammaire Gn sans récursivité à gauche
(1) Soit VN = {A1, A2, .. ., An}· On transformera G de telle sorte que si A~ a.,
alors a. commence, soit par a E VT, soit par Ai tel que j > i. A cet effet, on
pose i = 1.
(2) Soit Aï~Aïa.1 1... IAïClm 1 P1 1... 1 pp, où fü, ne commençant pas par Ak tel que
k ::> i. Remplacer les Aï-productions par : Aï ~P1l ... I PP 1 P1B 1... 1ppB
B~a.1 1... 1 Clm 1 a.1B 1... 1 CXmB, où B est un nouveau symbole non-terminal.
(3) Si i = n alors Gn est la grammaire résultante, et on s'arrête.
Sinon on pose i f- i+l et j f- 1.
(4) Remplacer chaque production de la forme Aï~Aia. comme suit:
Aï ~ P1a. 1... 1 Pma., où Aj ~ P1 1... 1 Pm E P. Les Atproductions
commencent par un terminal ou par Ak tel que k > j. Les Aï-productions
auront également cette propriété.
(5) Si j = i - 1 alors aller en (2)
Sinon j f- j + 1 ; aller en (4).
On donne l'exemple {S ~AB 1 a; A ~ BS 1 Sb; B ~ SA 1 BB 1 a} et on
applique scrupuleusement l'algorithme ci-dessus. On ordonne d'abord l'ensemble
VN tel que Ai = S, A2 = A et A3 = B, c'est-à-dire VN ={S, A, B}. Ensuite, on
applique la procédure pas à pas comme suit :
Avec i = 1 au niveau (2), il n'y a pas de changement pour S ~ AB 1 a
Le pas (3) donne if- i + 1=2etjf-1 qui fait passer en séquence à (4)
Au niveau (4), on obtient : A ~ BS 1 ABb 1 ab
Au niveau (5), on a j = i - 1, alors il faut aller à (2)
Au niveau (2) les règles A~ BS 1 ab 1 ABb, donnent les règles suivantes :
A ~ BS 1 ab 1 BSD 1 abD et D ~ Bb 1 BbD.
Au niveau (3) i = 2 -:t- 3 alors on pose i f- i + 1 qui donne 2+1 = 3 ; j f- 1 et on
passe en séquence à ( 4)
Au niveau (4), on obtient B ~ ABA 1 aA 1 BB 1 a
En (5), j = 1 i- i - 1 qui donne 2 alors j f- j+l qui donne 2 ; aller à (4).
En (4), on obtient B ~ BSBA 1abBA1BSDBA1abDBA1aA1BB1 a
j = 2 = i - 1 = 3 - 1 qui donne 2, alors Ok ! Donc aller à (2)
En (2), on obtient finalement les deux règles suivantes à partir de B :
B ~ abBA 1 abDBA 1 aA 1 a 1 abBAE 1 abDBAE 1 aAE 1 aE
E ~ SBA 1 SDBA 1 B 1 SBAE 1 SDBAE 1 BE.
On passe en séquence au niveau (3), et ici on ai= 3 = n, donc on s'arrête.
En récapitulant, on obtient la grammaire Gn = (VN, VT, P, S) avec :
VN= {S, A, B, D, E}
VT ={a, b}
P = {S ~AB 1 a
A ~ BS 1 ab 1 BSD 1 abD
D ~ Bb 1 BbD
B ~ abBA 1 abDBA 1 aA 1 a 1 abBAE 1 abDBAE 1 aAE 1 aE

www.bibliomath.com
Rappels sur les langages formels 25

E ~ SBA 1 SDBA 1 B 1 SBAE 1 SDBAE 1 BE}


Un autre exemple plus simple est celui des expressions arithmétiques dont les
règles sont {S ~ S + T 1 T ; T ~ T * F 1 F ; F ~ (S) 1 a} où S est l'axiome.
Cette grammaire est récursive à gauche, étant donné qu'elle possède les règles
S ~ S + T, T ~ T * F qui présentent une récursivité à gauche immédiate. On
parle de récursivité immédiate lorsque celle-ci apparait directement dans la règle,
c'est-à-dire, la règle est de la forme A ~ Aa. E P. Ce type de récursivité est simple
à neutraliser et ne nécessite pas d'appliquer pas-à-pas toute la procédure
précédente. On applique simplement le point (4) aux règles concernées. Les règles
F ~ (S) 1 a, ne sont pas concernées. On applique donc la procédure uniquement
aux règles T ~ T * F et S ~ S + T.
Ainsi, d'après le point (4) de l'algorithme, T ~ T * F 1 F, donne T ~ F 1 FB et
B ~ *F 1 *FB, et S ~ S + T 1 T, donne S ~ T 1TA et A~ +T l+TA.
En récapitulant, on a la grammaire Gr= (VN, VT, P, S) finalisée sans récursivité à
gauche, avec les éléments suivants :
VN= {S, T, F, A, B}
VT= {a, +, *, (,)}
P = {S ~ T 1 TA; A~ +T 1+TA;T~F1 FB; B ~ *F 1 *FB; F ~ (S) 1
a}.
Remarque 3.3
Une grammaire mise sous la forme normale de Greibach est une grammaire sans
récursivité à gauche. La mise sous FNG d'une grammaire, nécessite que la
grammaire soit non récursive à gauche au préalable. En effet, il est plus aisé de
travailler avec une grammaire non récursive à gauche, dont les règles sont déjà
transformées et faciles à manipuler comme {S ~ T 1 TA, A~ +T 1 +TA;
T ~ F 1 FB ; B ~ *F 1 *FB ; F ~ (S) 1 a}, qu'avec son équivalente récursive à
gauche S ~ S + T 1 T; T ~ T * F 1 F; F ~ (S) 1 a.
La FNG de S ~ T 1 TA; A ~ +T 1 +TA; T~ F 1 FB, B ~ *F 1 *FB;
F ~ (S) 1 a, est donnée par les règles suivantes :
F ~(SC 1 a
c~)
B ~ *F J *FB
T ~ a 1 aB 1 (SC 1 (SCB
A~+T 1 +TA
S ~ a 1 aB 1 (SC 1 (SCB 1 aA 1 aBA 1 (SCA 1 (SCBA
On obtient à la fin, la grammaire Gg = (VN, VT, P, S) définie par les éléments
suivants:
VN= {S, T, F, A, B, C}
V T = {a, +, *, (, )}
S ~ a 1 aB 1 (SC 1 (SCB 1 aA 1 aBA 1 (SCA 1 (SCBA
A~+T 1 +TA
T~ a 1 aB 1 (SC 1 (SCB
B ~ *F J *FB
www.bibliomath.com
26 Chapitre 1

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 :

Soit le système d'équation P = PR ® N dont le point fixe est P = NR* où


R* = I ® R ® R 2 ® R 3 ® ... ; I est la matrice identité (avec e sur la diagonale et 0
partout ailleurs).
Mais, comme R + = RR *, alors on réécrit le système P = PR ® N = NR*R ® N
= NR+ ® N. On ne peut pas trouver une grammaire correspondant à ces
équations, car R * correspond à un ensemble infini de termes. On peut cependant
remplacer R+ par Q. Ainsi, puisque R+ = RR+ ® R, alors on a: Q = RQ ® R.
En reconsidérant les deux systèmes correspondant à P et Q on a :
P = NR+ ® N = NQ ® N
Q = RQ ® R

[A 8] = [1 OA ffi O] [~ ~1 E9[1 OA ffi O]

[~ ~1 = [0: :11 [~ ~1 œ[0: :11


qui s'écrivent sous forme de règles de production comme suit :
A ~ lX 1 OAZ 1 OZ 1 1
B ~ 1Y 1 OA T 1 OT 1 OA J 0
X~ OBX 1 OB
Y~OBY
Z ~ AlZ
T ~ AlT 1 Al

Pour finaliser, il faut transformer les règles Z ~ AlZ et T ~ Al T 1 Al pour


obtenir finalement la grammaire G = (VN, VT, P, A) avec l'ensemble des règles P
suivant:
www.bibliomath.com
Rappels sur les langages formels 27

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

Mais a-t-on aussi L• ;;;i (L •) • ?


Soit w E (L*)*, alors dans ce cas w E uj~o(L*)Ï, c'est-à-dire 3 j ~ 0 t.q w E (L*)i,
donc w = x1x2 ... xi, avec Xn E L*; ce qui entraine qu'on a Xn E Ui~oLi qui signifie
que w E uk~O Lk ' c'est-à-dire que w E L*. Donc, on a bien (L *)*ç;;; L*.

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

2°/ Suppression de la récursivité à gauche de la grammaire obtenue en 1.


On a le choix, soit on applique l'algorithme classique de suppression de la
récursivité à gauche, soit on applique la transformation sous forme normale de
Greibach (FNG). Ici, on applique l'algorithme classique.
On ordonne d'abord l'ensemble VN tel que Ai = S et A2 = A ; on a alors,
VN= {S, A}. Ensuite, on applique la procédure pas à pas comme suit :
Les règles S ~ Sa 1 Ab 1 b 1 a, seront remplacées par S ~ Ab 1 b 1 a 1 AbB 1 bB 1
aB; B ~a 1 aB.
En ce qui concerne A ~ Sa 1 Ab 1 b, on a une récursivité gauche immédiate A ~
Ab, mais l'autre règle A ~ Sa ne permet pas d'agir comme dans le cas précédent.
L'ordre de S dans l'ensemble VN est plus petit. Il va donc falloir remplacer S dans
la règle A~ Sa par les règles S ~Ab 1 b 1 a 1 AbB 1 bB 1 aB. La règle A~ Sa
sera donc remplacée par A ~ Aba 1 ba 1 aa 1 AbBa 1 bBa 1 aBa. On est
maintenant dans le cas où l'on peut supprimer la récursivité gauche immédiate du
symbole A.
En récapitulant, on obtient les règles A ~ Ab 1 Aba 1 AbBa 1 ba 1 aa 1 bBa 1 aBa
1 b, qui donnent directement A ~ ba 1 aa 1 bBa 1 aBa 1 b 1 baC 1 aaC 1 bBaC 1
aBaC 1bC;C~b1ba1bBa1bC1baC1 bBaC.
La grammaire sans récursivité à gauche est G 1 = (VN, VT, P, S), avec
VN = {S, A, B, C} et le nouvel ensemble Pest :

S ~ Ab 1 b 1 a 1 AbB 1 bB 1 aB
B~ajaB

A ~ ba 1 aa 1 bBa 1 aBa 1 b 1 baC 1 aaC 1 bBaC 1 aBaC 1 bC


C ~ b 1 ba 1 bBa 1 bC 1 baC 1 bBaC.

3° / Transformation de la grammaire trouvée en 2 sous la forme normale de


Greibach (FNG).
La règle B est déjà sous (FNG) ; pour les autres règles S, A et C, on introduit les
règles auxiliaires X ~ a et Y ~ b permettant de mettre les autres règles sous la
forme normale de Greibach. On a alors les règles ci-après :
C ~ b 1 bXj bBX 1 bC 1 bXC 1 bBXC
A ~ bX 1 aX 1 bBX 1 aBX 1 b 1 bXC 1 aXC 1 bBXC 1 aBXC 1 bC
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:

S ~ bXY 1aXY1bBXY1 aBXYj bY 1bXCY1aXCY1bBXCY1aBXCY1


bCYjbXYBjaXYBjbBXYBjaBXYBjbYBjbXCYBjaXCYBjbBXCYB
jaBXCYBjbCYBjbjajbBjaB

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.

On procède par des substitutions afin de se ramener progressivement à la


forme souhaitée comme suit :
S -7 AY 1 b 1 a 1 AT 1 YB 1 XB
Y-7 b
X-7 a
T-7YB
B-7alXB
A -7 YX 1 XXI YBX 1 XBX 1 b 1 YXC 1 XXC 1 YBXC 1 XBXC 1 YC, qui sont
transformées en les règles :
A -7 YX 1 XXI YE 1XE 1 b 1YF 1 XFI YG 1 XG 1YC
E-7 BX
F-7 XC
G-7 BF

Les règles C -7 b 1 ba 1 bBa 1 bC 1 baC 1 bBaC, seront d'abord transformées en


les règles C -7 b 1 YX 1 YBX 1 YC 1 YXC 1 YBXC, qui donneront, après
substitution, les règles suivantes :
C -7 b 1 YX 1 YE 1YC 1 YF 1YG.

En récapitulant l'ensemble des transformations on obtient la grammaire sous


FNC avec l'ensemble des règles suivant :

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

Les formalismes les plus répandus pour la représentation des langages de


type 3 sont les grammaires régulières, les automates d'états finis et les
expressions régulières. On peut tout aussi bien, au besoin, représenter ces
langages au moyen de diagrammes syntaxiques.

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

1.1 Exemples de grammaires régulières


Soit G = (VN, VT, P, S) une grammaire qui génère les nombres entiers naturels.
VT = {O, 1, 2, 3, 4, 5, 6, 7, 8, 9}
P = {S ~ a S a } avec a E V T·
1

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

Figure 7 : Arbres syntaxiques des mots "l 2 3 4" et "1 1 0"

Soit G = (V N, V T, P, I) la grammaire régulière qui génère les identificateurs de


longueur quelconque. En général, les identificateurs sont des mots formés de
lettres alphabétiques et de chiffres décimaux, commençant obligatoirement par
une lettre alphabétique.
VN = {I}
VT ={O ... 9, a ... z}
I est l'axiome
P = {I ~ a 1 b l···I z 1 la 1 Ib ... I Iz 1 IO 1 Il 1 ... 1 19}
Cette grammaire est régulière à gauche, c'est-à-dire que les non-terminaux, dans
les membres droits, apparaissent tous au début du mot.
La grammaire régulière G = (V N, VT, P, I) génère les identificateurs de
longueur comprise entre 1 et 4.
VN = {I}
VT = {0 ... 9, a ... z}
I est l'axiome
P = {I ~ a 1 b ... 1 z 1 aJ 1 bJ .. -1 zJ
J ~a 1b ... Iz1 aK 1bK I··· 1 zK 1 0 l 1 l···I 9 1 OK l lK 1... 1 9K
K ~a 1 b ... I z 1 aL 1 bL ... I zL 1 0 l 1 1- .. 1 9 1 01 l 111 211 ... 1 91
L ~a 1 b l···I z 1 0 l 1 1- .. 1 9}.

2 Automates d'états finis


Définition 2.1 (Automate d'états finis ou automate fini)
Un automate d'états finis est une machine abstraite définie par le 5-uplet
A = {S, so, VT, F, I) où:
S est un ensemble fini et* 0 des états de l'automate.
s0 E S est l'état initial unique de l'automate.
F est l'ensemble des états finals F ~ S
VT est un alphabet terminal fini et* 0.
I est la fonction de transition définie comme suit :
1ercas si l'automate est simple, alors SxVT~f<J{S).
2eme cas si l'automate est partiellement généralisé, alors
SxVTu{E} ~ p{S).
www.bibliomath.com
Langages réguliers 35

3eme cas si l'automate est généralisé alors SxVT* ~p(S).


On notera cette fonction par I, Ie et 1* respectivement pour le 1er, 2eme et 3eme
cas.
Remarque 2.1
p(S) représente l'ensemble des parties de S. Ce qui signifie qu'à un état donné Si
de l'automate A, et pour un même symbole de transition a E (VT ou VTu{e} ou
VT*), la transition peut avoir lieu vers plusieurs états lorsque l'automate n'est pas
déterministe. Si l'automate est déterministe, au lieu de l'ensemble p(S),
l'ensemble d'arrivée sera tout simplement S.
On définira dans les paragraphes suivants, ce qu'est un automate d'états
finis déterministe. En ce qui concerne les termes généralisé, simple et
partiellement généralisé, c'est relatif au symbole qui force la transition. En effet,
si le symbole x E VT, la transition est simple, et elle est notée I (si, x) = Sj.
si le symbole x E VTv{e}, on peut admettre des transitions spontanées du
genre I (si, e) = si forcées sans lecture d'un symbole de l'ensemble VT.
si l'élément x E VT *, on peut rencontrer plusieurs sortes de transitions,
notamment celles dont les éléments qui forcent la transition sont de longueur
supérieure à 1, comme, par exemple, I (si, ab) = Sj où labl = 2.
En pratique, notamment pour l'implémentation sur machine, on se ramène
toujours à un automate simple, qui est plus facile à manipuler.
2.1 Notations
Plusieurs notations ont été proposées dans la littérature pour représenter la
fonction de transition d'un automate d'états finis. On décrira succinctement ces
notations. Pour cela, on considère les éléments suivants :
s : état de départ ;
q : état d'arrivée ;
a : élément forçant une transition ;
I : la fonction de transition.
Représentation fonctionnelle
C'est la notation la plus largement utilisée, on la note par I (s, a) = q.
Triplet (s, a, q)
C'est une configuration qui est employée le plus souvent pour contrôler
manuellement les transitions ou l'évolution d'une analyse par l'automate.
Instructions
Il existe deux ou trois possibilités d'écriture mais qui sont plus ou moins proches,
à savoir, s ~ q ou bien s ~ aq ou encore sa ~ q . Les deux dernières
possibilités rappellent la notation des grammaires régulières. D'ailleurs, on pourra
s'en inspirer pour effectuer le passage d'une grammaire régulière à l'automate
d'états finis équivalent (voir section 4 du présent chapitre).
Représentation matricielle
C'est une représentation sensiblement proche de la représentation matricielle d'un
graphe, sauf que pour ce dernier on a une matrice carrée ayant pour lignes et
colonnes les sommets du graphe, alors que pour l'automate, qu'on verra sous peu,
www.bibliomath.com
36 Chapitre 2

c'est un peu différent. On note la matrice par 1 comme la fonction de transition.


Elle coïncide avec la notation fonctionnelle 1 (s, a) = q. En d'autres termes, les
lignes et colonnes de la matrice 1 sont indexées respectivement par S et VT· Les
valeurs de la matrice 1 E p(S) ; ce qui est tout à fait conforme à la définition de
la fonction de transition 1: SxVT~.f<J(S). Le Tableau IV schématise la structure
de représentation de la matrice de transition d'un automate fini.
a X
1
1 0
so 1
1
1 1
St -------''-'----- --i----------------------....1
1 ••• t
1 0
1
1

s---------------1 q

0 0

Tableau IV- Matrice de transition d'un automate fini

L'automate fini A = (S, s0 , VT, F, 1) est représenté par la matrice de transition du


Tableau V. S = {O, 1, 2, 3, 4} ; s0 = 0; F = {4}. La fonction de transition est
définie par les valeurs : 1 (0, a) = {O, 1} ; 1 (0, b) = {O, 2} ; 1 (0, c) = {O, 3} ;
1(1, {a, b, c}) = {1} ; 1 (1, a) = 4 ; 1 (2, {a, b, c}) = {2} ; 1 (2, b) = 4 ;
1 (3, {a, b, c} = {3}; 1 (3, c) = 4.
a b c

0 {O, 1} {O, 2} {O, 3}

1 {1, 4} 1 1

2 2 {2, 4} 2

3 3 3 {3, 4}
4 0 0 0

Tableau V- Représentation matricielle de l'automate fini A

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.

Figure 8 : Représentation graphique d'une transition dans un automate fini

En ce qui concerne l'état initial, il est préfixé par une double flèche, comme
sur la Figure 9.

Figure 9 : Représentation graphique de l'état initial d'un automate

L'état final est noté par un double cercle concentrique comme illustré par la
Figure 10.

Figure 10 : Représentation d'un état final d'un automate

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.

Figure 11 : Etat simultanément initial et final d'un automate

Par exemple, soit à construire l'automate du langage L = {O, 1}*, et à


exprimer sa fonction de transition dans le modèle graphique.
L = {O, 1}* étant l'ensemble des chaînes formées à base de l'alphabet {O, 1},
auquel on ajoute la chaîne e. L'automate d'états finis reconnaisseur est défini par
le 5-uplet A = (S, s0 , VT, F, I) tel que :
S = {s} qui est l'ensemble des états de l'automate ;
s est l'état initial de l'automate ;
VT = {O, 1} étant l'alphabet terminal ;
www.bibliomath.com
38 Chapitre 2

F = {s}, l'ensemble des états finals, qui contient l'unique état « s ».


On constate que l'unique état final de l'automate est confondu avec l'état initial
de l'automate, ceci parce que le langage L (A) contient ë.
La fonction de transition I : {s} x {O, 1} ~ {s} est représentée graphiquement par
le diagramme de la Figure 12.

0, 1

Figure 12 : Diagramme de transition pour L = {O, 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} *.

2.2 Déterminisme d'un automate d'états finis


On note le cardinal (le nombre d'états de destination à partir d'un état donné
« s ») de la fonction de transition I(s, a) par le terme II(s, a)j.
Automate simple
Un automate simple est déterministe si V s e S et a e VT, on a II(s, a)I :5 1.
Autrement dit, il y a au plus un seul arc qui sort de l'état « s » par le symbole
"a" qui force la transition.
On peut tout aussi écrire V (set q e S et a e VT), soit la fonction I(s, a) = q,
soit la fonction I(s, a) = 0. La fonction de transition est plutôt notée par
I: SxVT ~ S, au lieu de SxVT ~p(S), puisque s'il existe une transition à partir
d'un état donné pour un symbole donné, elle doit être unique.
Soit à construire un automate d'états finis simple déterministe qui accepte les
nombres entiers pairs. Pour une meilleure lisibilité, il est commode d'utiliser la
notation graphique. Pour construire cet automate, on part de l'idée qu'un nombre
entier pair se termine toujours par 0, 2, 4, 6, 8. Donc, les transitions vers l'état
final se feront toujours par les chiffres 0, 2, 4, 6 et 8. Les autres transitions
peuvent être effectuées avec n'importe quel chiffre décimal de 0 à 9. L'automate
est défini par le 5-uplet A = (S, s0 , VT, F, I) tel que :
S = {s, q};
s est l'état initial ;
F = {q};
VT = {O, 1, 2, 3, 4, 5, 6, 7, 8, 9} ;
La fonction de transition I est définie par les instructions suivantes :
I (s, {O, 2, 4, 6, 8}) = q ; I (s, {1, 3, 5, 7, 9}) = s ;

www.bibliomath.com
Langages réguliers 39

I (q, {O, 2, 4, 6, 8}) = q; I (q, {1, 3, 5, 7, 9}) = s.


Le diagramme de transition de l'automate est illustré par la Figure 13.

1, 3, 5, 7, 9

1, 3, 5, 7, 9

Figure 13 : Diagramme de transition pour les entiers naturels pairs

Automate partiellement généralisé


Un automate partiellement généralisé est déterministe si V s E S et a E VT u{E}
on a IIE(s, a)I ::;; 1. Autrement dit, pour un état donné s E S et a E VTu{E}, il y a
au plus un seul arc (une seule transition au plus est autorisée) sortant de l'état s.
De plus, V (s E Set a E VT) :
si IIE(s, a)I = 1, alors on a nécessairement IIE(s, e)I = 0
si IIE(s, e)I = 1, alors on a nécessairement IIE(s, a)I = 0
On note la fonction de transition par IE: SxVTu{e}~p(S).

Par exemple, soit l'automate dont les transitions sont données par le
diagramme de la Figure 14.

Figure 14 : Diagramme de transition d'un automate fini

Cet automate présente certaines incommodités (transitions spontanées) qui


rendent son implémentation plus complexe. Pour y remédier, il faut se ramener à
un automate simple qui est naturellement plus facile à manipuler. Présentant à la
fois des transitions simples et des e-transitions (transitions spontanées) à partir
d'un même état, cet automate est considéré comme non déterministe par
définition. Pour le rendre simple, il suffit de supprimer les e-transitions en
appliquant les points suivants :
V (s, p, q E Set a E VT u{e}), si (IE(s, e) =pet IE(p, a) = q), on a alors
I(s, a) = q. Ainsi, la transition IE(s, e) = p disparaît.
V s E S, si on a IE(s, e) = p et si p E F, alors l'état s devient lui aussi état
final.
www.bibliomath.com
40 Chapitre 2

On applique cette procédure pour l'automate de la Figure 14. Sur la base de


la représentation graphique on a la représentation fonctionnelle qui s'écrit comme
suit:
Ie(so, b) = so; IE(so, e) = s1;
IE(s1, a)= so; Ie(si, b) = Sf; IE(s1, e) = Sf;
IE(st, a) = Sf; IE (st, e) = so.
Pour obtenir l'automate sans transitions spontanées, on applique
scrupuleusement la procédure sur les valeurs de la fonction IE précédente. Mais, on
doit procéder par étape.
On supprime d'abord la transition 1 (s 0 , e) = s1 comme suit :
Ie(so, e) = s1 et IE(si, e) = Sf, ce qui donne I(so, e) = St
IE(so, E) = s1 et IE(si, a) = so, ce qui donne I(so, a) = so
IE(so, E) = s1 et IE(s1, b) =St, ce qui donne I(so, b) = Sf.

A la fin de cette étape, on obtient le graphe intermédiaire de la Figure 15.

b, e

a
e
a, b

Figure 15: Diagramme de transition de l'automate de la Figure 14


semi transformé

Etant donné l'existence d'une transition spontanée de s 0 à Sf, alors l'état s0


devient initial et final à la fois. On poursuit l'application de la procédure comme
suit:
Suppression de la deuxième transition spontanée I(s 0, E) = Sf
I(so, E) = Sfet I(st, a)= St, ce qui donne I(so, a)= Sf
I(so, e) = St et I(st, e) = so, ce qui donne I(s 0 , E)= s0 . Cette transition devient
superflue, elle doit disparaître, car s0 est devenu simultanément initial et final.
Pour ce qui est de la dernière transition spontanée à supprimer on a :
IE(st, e) = so et I(so, a) = so, ce qui donne I(st, a) = so
Ie(st, E) = so et I(so, b) = so, ce qui donne I(st, b) = so

A la fin du processus on a obtenu l'automate simple dont la fonction est exprimée


par le graphe de la Figure 16.

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

Figure 17 : Diagramme de transition de L = {{ab, ba} {ab, ba} n 1 n ~ O}

Comme préconisé, il faut décomposer les transitions de longueur supérieure à 1


en transitions de longueur 1. Pour cela, on introduit deux nouveaux états s1 et s2 ,
dans l'ensemble S.

www.bibliomath.com
42 Chapitre 2

h{so, ab) = Sf et h (s 0 , ba) = Sf, se décomposent respectivement comme suit :


I{so, a) = s1
l{s1, b) =St
I{so, b) = s2
I(s2, a) =St
h{st, ab) = Sf et h{st, ba) = Sf donnent respectivement les instructions suivantes :
I(st, a) = s1
I(s1, b) =St {déjà trouvée)
I(st, b) = s2
I(s2, a) = Sf {déjà trouvée également)
Il n'y a pas d'e-transitions. Par conséquent, i. est complètement transformée.
Son graphe est celui de la Figure 18.

Figure 18 : Diagramme de transition de la Figure 17 transformé

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.

2.3 Transformation d'un AFN en AFD


La procédure la plus connue et la plus simple est celle qui consiste à considérer un
automate simple non déterministe en entrée, et à le transformer en son homologue
simple et déterministe.
Procédure de déterminisation d'un automate fini
Soit A = (S, so, VT, F, I) un automate simple non déterministe et soit
A' = {S', so', VT', F', 1') son équivalent déterministe.
Cette équivalence a lieu, si et seulement si on vérifie que :
VT' = VT
S' ç; p{S) avec S' qui est un sous-ensemble des parties de S

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}.

Par exemple, soit à rendre déterministe l'automate de la Figure 19.


Pour faciliter cette opération, il est fortement recommandé d'utiliser la notation
matricielle afin de pouvoir repérer plus facilement les transitions multi définies
(non déterministes). A cet effet, le graphe de l'automate de la Figure 19 est
traduit sous forme matricielle, comme illustré par le Tableau VI.

a
b
b

Figure 19 : Diagramme de transition de L = {a {a, b} n 1 n ~ 0}

a b

s s, p 0

p
0 s, p

Tableau VI- Matrice de transition correspondant au diagramme de la


Figure 19

D'après la procédure de déterminisation, un élément de S' fera participer des


éléments de S dans la construction de la nouvelle matrice de transition. Cette
dernière représente l'automate déterministe recherché. Il s'agit donc de démarrer
avec la 1ere ligne de la matrice repérée par l'état s, et dès qu'un nouvel état
comme {s, p} apparait, une nouvelle ligne nommée {s, p} est créée. Ce processus
est exécuté conformément aux orientations de l'algorithme ci-dessus, pas à pas
jusqu'à ce qu'il ne soit plus possible d'introduire de nouvelle ligne.
En effet, quand il n'y a plus de nouveaux états générés qui participent à former de
nouvelles lignes, on s'arrête. C'est le cas de cet exemple, car comme on peut le
voir, à part l'état {s, p}, il n'y a plus de nouveaux états générés. Donc, la
construction est achevée, et la matrice recherchée est celle du Tableau VII.
On détermine le nouvel état final conformément à la procédure de
déterminisation. L'état final est donc {s, p}, parce que p E F et
{s, p} n F = {p}:;: 0.

www.bibliomath.com
44 Chapitre 2

a b

s {s, p} 0

{s, p} {s, p} {s, p}

Tableau VII- Matrice de transition déterministe du langage L

A présent, il ne reste plus qu'à procéder au renommage des états et à


construire le graphe pour mieux voir la différence avec l'automate d'origine.
Si on désigne l'ensemble {s, p} par q. L'automate déterministe recherché est
celui représenté par le graphe de la Figure 20.

~~
Figure 20 : Diagramme de transition déterministe de L

Pour mieux comprendre comment appliquer la procédure de déterminisation,


on propose un deuxième exemple. Soit alors la matrice de transition du
Tableau VIII qui représente un automate fini noté AT qui reconnait les chaines
sur l'alphabet {O, 1} dont le dernier caractère est apparu au moins une fois.

0 1
So {so, s1} {so, s2}
{si, SF} {si}
{s2} {s2, SF}
0 0

Tableau VIII- Matrice de transition de l'automate fini AT

Tout comme la 1ere ligne de la matrice du Tableau VIII, la 1ere ligne de la


nouvelle matrice contient également les éléments {s0 , si} et {s0 , s2 } comme illustré
par le Tableau IX. On utilise {s0 , s1} et {s0 , s2} pour générer de nouvelles lignes,
et si de nouveaux éléments apparaissent, ils seront à leur tour utilisés pour
marquer de nouvelles lignes, et ainsi de suite, jusqu'à ce qu'il n'y ait plus de
nouveaux éléments à générer.

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}

Tableau IX- Matrice de transition déterministe de l'automate AT

Pour dessiner un diagramme de transition clair et concis, il est recommandé de


renommer au préalable les états du Tableau IX. Après cette opération, on obtient
le Tableau X, ainsi que le diagramme de la Figure 21. Les états C, E et G
associés respectivement aux états finals {so, s1, SF}, {so, s2, SF} et {so, s1, s2, SF}
sont évidemment des états finals. L'état initial est notés.

0 1
s A B
A c D
B D E
c c D
D E
E
G G
D
G G
G

Tableau X- Matrice de transition déterministe de AT version finalisée

0, 1

Figure 21 : Diagramme de transition déterministe de l'automate AT

www.bibliomath.com
46 Chapitre 2

2.4 Minimisation du nombre d'états d'un automate fini


Réduire le nombre d'états d'un automate A, revient à trouver la couverture
minimale de A ou l'automate canonique qu'on notera Ac. Le besoin de
minimisation s'impose toujours pour se débarrasser des états superflus ou
redondants. Une fois l'automate minimisé, son l'implémentation en machine
devient moins encombrante.
Un état est inutile s'il est inaccessible, c'est-à-dire, aucun arc n'arrive à cet
état.
Un état superflu n'est pas forcément inutile, il peut par exemple correspondre
à un état redondant, équivalent à un ou plusieurs autres états dans l'automate.
On verra sous peu comment s'en débarrasser avec la procédure de réduction du
nombre d'états d'un automate.
On doit éliminer tous les états inutiles au préalable avant l'application de la
procédure de minimisation.

Définition 2.2 (Etats séparables)


Deux états qi et q2 distincts sont dits séparables, si 3 ro un mot de VT * qui les
distingue. Autrement dit, si I (q1, ro) = s1 et I (~, ro) = s2, alors si s1 E F alors
forcément s2 Il: F ou bien, si s2 E F alors nécessairement s1 Il!: F.

Définition 2.3 (Etats équivalents)


On dira que les états qi et q2 sont k-inséparables ou k-équivalents, et on écrit
qi =l q2, si et seulement s'il n'existe aucun mot ro, avec lrol : : ; k, qui les
distingue. On dit aussi que qi et q2 sont inséparables ou équivalents, si et
seulement s'ils sont équivalents pour tout k ~ O.
On va présenter l'algorithme qui permet de construire les classes d'équivalence
des états, progressivement en partant initialement avec deux classes, à savoir,
celle des états finals [F] et celle des états non-finals [S-F]. On dira par exemple,
qu'un mot de longueur nulle, donc par la congruence =.0 , définit les états des
classes d'équivalence [S-F] et [F] respectivement. L'algorithme de minimisation
suppose que l'automate présenté en entrée est déterministe et sans états inutiles.
Algorithme de minimisation du nombre d'états
Entrée : A = (S, sa, VT, F, I) un automate fini déterministe et sans états inutiles
Sortie: Am= (S', so',VT, F', I') un automate fini minimal tel que L (Am)= L(A)
qi=0 q2 <=>(q1 E Fetq2E F)oubien(q2!i!: Fetq1!1!: F).
Si on a (q1 =l q2) et si V a E VT, on a I (qi, a)= k I (q2, a), alors (q1 =k+l q2)·
Si on constate que, pour une classe donnée, les congruences =k et =k+l sont
identiques (stationnarité), on s'arrête pour cette classe et on passe à la
suivante.
Par exemple, soit à minimiser le nombre d'états de l'automate A1 représenté
par le diagramme de transition de la Figure 22.
Les états 5 et 6 sont des états inutiles car ils sont inaccessibles, donc ils seront
éliminés d'emblée. Les états 1 et 3 sont équivalents, car il n'existe aucun mot qui

www.bibliomath.com
Langages réguliers 47

les distingue. De même, en ce qui concerne 2 et 4. Il n'est pas difficile de déduire


graphiquement l'automate minimisé équivalent sans même utiliser l'algorithme de
minimisation précédent. En renommant les états {O}, {1, 3} et {2, 4}
respectivement par S, P et Q l'automate minimisé obtenu est celui donné par la
Figure 23.

Figure 22 : Diagramme de transition de l'automate A 1

b
a

Figure 23 : Diagramme de transition minimisé de l'automate A 1

On propose un autre exemple d'automate, qu'on note A2, représenté par le


graphe de la Figure 24. Ce deuxième exemple n'est pas trivial comme le premier,
pour être transformé intuitivement. On applique donc l'algorithme de
minimisation.

Figure 24 : Diagramme de transition de l'automate A2

=
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

Figure 25: Diagramme de transition minimisé de l'automate A 2

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

représente bien l'expression régulière «a», et 1 (st, a) = Sf représente l'expression


a*. Lorsqu'on concatène leurs graphes respectifs (ii) et (iii) on obtient, le graphe
(iv), c'est-à-dire celui de l'automate représentant l'expression régulière a+.

~Q
(i)
(ii)

6l
a
a
=>

(iii) (iv)

Figure 26 : Diagrammes de transition associés respectivement aux


expressions E, a, a* et a+

3.1 Lemme d'Arden


Si on a les règles A ~ a. 1 ~A (respectivement A ~ a. 1 A~), on déduit l'équation
A= a. ® ~A (respectivement A = a. ® A~) dont la solution est A = ~ *a.
(respectivement a.~*).
En particulier, si on a une règle A ~ ro 1 roA, on écrit l'équation A = ro®roA,
où ro est un élément de vT*, l'expression régulière qui en résulte s'écrit A = ro+.
En effet, si on a A = ro®roA, la solution de l'équation s'obtient en remplaçant A
récursivement par sa valeur. Autrement dit, A = ro®roA = ro®ro(ro®roA) = ro ®
ro2 Ef> ro2A = ro Ef> ro2 Ef> ro3 Ef> ro3 A = ro ® ro2 ® ro3 ® ... ® ron ... Ef> ... = ro+. L'expression
dénote effectivement le langage L = { ron 1 n ~ 1} qui correspond exactement au
langage qu'on aurait généré par dérivation à partir des règles A~ roA 1 ro.

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

solution est S = (ab® ba) +.

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

{} +k : représente la répétition au moins 1 fois et au plus k.


Par exemple, soit {S ~ S + A 1 A ; A~ (S) 1 a}, un sous ensemble de règles
d'une grammaire à contexte libre qui reconnait les expressions arithmétiques.
On peut transformer ces règles conformément aux conventions précédentes.
Le processus de dérivation donne S :::::> S +A:::::> S +A+ A:::::> ... :::::> S +A+ A+
A + A + A ... + A. On peut appliquer la 1 ere règle pour S autant de fois que l'on
désire. Ensuite, lorsqu'on applique la 2eme règle pour S, on obtient S :::::>+A + A +
A+ A+ A+ A... + A+ A= A {+A}+. Mais, si on avait d'emblée appliqué la
2eme règle, à savoir S ~ A, on aurait eu S :::::> A. En rassemblant les deux résultats
pour S, on aura S :::::>A {+A}+ 1 A, ou encore S ~A ({+A}+® i::) après mise en
facteur. Or V x, on sait que (x+® i::) = x*, donc ({+A}+®i::) = {+A}*. Par
conséquent, S ~A {+A}*. Pour les productions A~ (S) 1 a, si on remplace S par
sa valeur, on aura A~ (A {+A}*) 1 a.
La grammaire résultante est représentée par les règles S ~ A {+A}• ;
A ~ (A {+A}*) 1 a. Ces règles, comme on peut le constater, combinent la
notation conventionnelle avec le formalisme des expressions régulières.

4 Automates finis, grammaires et expressions régulières


Il existe une équivalence entre grammaires régulières, automates d'états finis et
expressions régulières. On décrira dans ce qui suit les règles de passage d'un
système à l'autre. Pour simplifier, on travaille avec des automates finis simples.
Donc, si on a des E-transitions, elles seront supprimées au préalable pour pouvoir
ensuite travailler avec des automates finis simples.

4.1 Passage de l'automate fini à une grammaire régulière


Pour mettre en œuvre l'équivalence entre l'automate fini AT = (S, s0 , VT, F, I) et
la grammaire régulière à droite G = (VN, VT, P, Z), on établit la correspondance
entre leurs éléments comme suit :
L'axiome Z de G sera représenté par l'état initial so de l'automate AT.
VT étant le vocabulaire terminal, le même pour les deux systèmes AT et G.
L'ensemble des règles de la grammaire est défini comme suit :
• On pose P f- 0 ;
• V 1 (s, a) = q, avec q ~ F alors P f- Pu{s ~ aq};
• V 1 (s, a) = q E F on a :
./ Si q est final simple, c'est-à-dire un état à partir duquel il n'y a aucune
transition ; alors P f- Pu{s ~ a}. On préfère dire état final simple,
plutôt que état final « puits » qui désigne généralement un état erreur .
./ Si q est final non simple, alors P f- Pu{s ~ a}u{s ~ aq}. Si la règle
{s ~ a} est déjà dans P, il n'est pas nécessaire de l'ajouter ; dans ce
cas, on a tout simplement P f- Pu{s ~ aq}.
Calcul de VN
• Si Sf est un état final simple, alors VN = S - {sf}.
• S'il n'existe pas d'état final simple, alors VN = S.
52 Chapitre 2

Par exemple, on se propose de déduire une grammaire régulière à droite G


équivalente à un automate fini AT dont la fonction de transition est définie par :
I(s 0 , b) = si; I(si, a) = {si, sr}; I(sr, b) = sr, où s0 est l'état initial et F =
{si, sr} l'ensemble des états finals.
En vertu de la procédure de passage présenté ci-dessus, la grammaire et régulière
à droite équivalente est G = ({ s0 , si, sr} ; {a, b} ; P ; s0 ) avec P = { s0 ~ b 1 bsi ;
si~ a 1 asi 1 asr; Sf ~ b 1 bsr}.

En renommant les symboles de VN, on aura les règles


P = {Z ~ b 1 bA ; A~ a 1 aA 1 aB ; B ~ b 1 bB}.

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}.

4.2 Passage de la grammaire régulière à l'automate fini


Soient G = (VN, VT, P, Z) une grammaire régulière à droite et
AT= (S, s0 , VT, F, I) un automate fini simple. La grammaire G est équivalente à
l'automate AT, c'est-à-dire L(G) = L(AT), si on a:
Z l'axiome de G devient l'état initial de l'automate AT.
VT l'alphabet terminal qui est le même pour les deux systèmes G et AT.
La fonction de transition 1 est telle que :
• Pour tout A~ aB e P, on a la transition 1 (A, a)= B.
• Si A ~ a e P sans avoir en même temps A ~ aX e P, alors l'état X sera
forcément un état final simple. 1 (A, a) = X e F et S = VN u{X}. S'il
n'existe pas d'état final simple alors S = VN.
• Si A ~ a 1 aY e P, et si 3 B ~ bY e P, sans qu'on ait simultanément
B ~ b e P, alors 1 (A, a) = {q, Y} où q est un final simple et Y un état
ordinaire.
• Mais, si A ~ a 1 a Y e P, et si V B ~ b Y e P on a en même temps
B ~be P, alors Y est un état final (non simple), et donc 1 (A, a) =Y.
• Si Z ~ E e P, c'est-à-dire E e L(G). Autrement dit, Z est un état initial et
final à la fois. On peut donc avoir B ~ bZ, sans avoir explicitement B ~ b
e P, alors 1 (B, b) = Z.
Par exemple, soit la grammaire régulière à droite G = (VN, VT, P, Z) qui
génère le langage L(G) = {anbm, c 1 n ~ 0 et m ~ l}.
P = {Z ~ c 1 aA 1 bB 1 b ; A ~ aA 1 b 1 bB ; B ~ b 1 bB} ;
VN ={A, B, Z}; VT ={a, b, c}.
On applique la procédure précédente. On a alors la règle Z ~ c, et il n'existe
pas de règle Z ~ cC, alors C constitue forcément un état final simple. Par
conséquent, l'ensemble des états S = VNu{C}. Z est l'état initial de l'automate
AT. F = {C, B} est l'ensemble des états finals.
1 est la fonction de transition définie par :
I(Z, c) = C; I(Z, a) =A; (Z, b) = B
I(A, b) = B; (A, a)= A; I(B, b) = B
On propose un exemple où il n'y a pas d'état final simple. Par exemple, soit la
grammaire régulière à droite qui engendre le langage L(G) = {anbm 1 n, m ~ O}
dont P = {Z ~ E i aZ i b i bA; A~ b i bA} où VT ={a, b} et VN = {Z, A}. On
cherche l'automate AT tel que L (AT) = L (G).
Z, l'axiome de G, constituera l'état initial de l'automate AT. Il n'y a pas d'état
final simple, car il n'y a pas de règle de la forme A ~ x sans la règle A ~ xQ en
même temps. Par conséquent, l'ensemble des états de l'automate AT est S = VN =
{Z, A}. Mais, comme Z ~ E, alors Z est un état à la fois initial et final. La
fonction de transition 1 est exprimée par le graphe de la Figure 27.
54 Chapitre 2

A titre d'exercice de réflexion, on laissera le soin au lecteur d'établir lui-même


le passage d'une grammaire régulière à gauche à l'automate d'états finis
équivalent.

Figure 27: Diagramme de transition de L(G) = {anbm 1 n, m;?: O}

4.3 Passage d'une grammaire régulière à droite à son équivalente


régulière à gauche
Soit G une grammaire régulière à droite. On doit suivre les points suivants pour
construire une grammaire régulière à gauche équivalente :
Construire l'automate A fini pour G ;
Construire l'automate fini miroir AR de l'automate A ;
Construire la grammaire régulière GR à droite à partir de AR ;
Déduire la grammaire Ga, miroir de GR qui est la grammaire régulière à
gauche recherchée.
Par exemple, soit la grammaire régulière à droite dont les règles sont
Z ~ aB 1 b 1 a ; B ~ b 1 bB, qui génère le langage L(G) = {b, abn 1 n;?: O}
L'automate A équivalent est représenté par le graphe de la Figure 28.

Figure 28: Diagramme de transition de L(G) = {b, abn 1 n;?: O}

Figure 29: Diagramme de transition miroir de L(G) = {b, abn n;?: O}

Pour construire l'automate miroir AR correspondant, on applique


successivement les points suivants :
Inverser l'orientation de toutes les flèches de transition de sorte que la
transition I (s, a) = q, produise la transition inverse IR( q, a) = s.
L'état initial devient final unique.
Langages réguliers 55

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.

Figure 30: Diagramme de la Figure 29 transformé (sans &-transitions)

Une fois spécifié l'automate miroir, on extrait la grammaire régulière à droite


correspondante conformément à la procédure de passage décrite précédemment en
section 4.1 du présent chapitre. La grammaire régulière à droite équivalente est
décrite par les règles suivantes :
Sa~ a 1b1 bB
B ~a 1 bB
Pour obtenir la grammaire régulière à gauche équivalente recherchée, il suffit
d'inverser (au sens miroir), les membres droits des règles de la grammaire
régulière à droite ci-dessus. Ce qui donne les règles suivantes :
Sa~ a 1b1 Bb
B ~a 1 Bb

4.4 Passage d'une grammaire régulière à gauche à son équivalente


régulière à droite
Soit G une grammaire régulière à gauche ;
- Construire la grammaire GR miroir de G ;
Construire l'automate A équivalent à GR;
Construire l'automate miroir AR de A ;
Construire la grammaire équivalente à AR.
Par exemple, soit la grammaire régulière à gauche qui engendre les nombres
binaires pairs. Ses règles de production sont :
Z ~ 0 1 BO
B ~ 0 1 1 1 Bl 1 BO
La grammaire miroir Gm correspondante est décrite par les règles suivantes :
Z ~ 01 OB
B ~ 0 1 1 1 1B 1 OB
56 Chapitre 2

L'automate de cette dernière se construit selon la méthode décrite


précédemment. Ce qui donne le graphe de la Figure 31.

0, 1
0

Figure 31 : Diagramme représentant la grammaire Gm des binaires pairs

Ensuite, il faut déduire le miroir du diagramme de la Figure 31 par la méthode


précédente, ce qui donne le graphe de la Figure 32.

Figure 32: Diagramme miroir du diagramme de la Figure 31

Après transformation (suppression des e-transitions), le graphe devient celui de la


Figure 33.

Figure 33: Diagramme sans e-transitions du diagramme de la Figure 32

A présent, il ne reste plus qu'à extraire la grammaire régulière à droite


recherchée. Ses règles sont So ~ 0 1 OB 1 1B ; B ~ OB 1 1B 1 O. Le symbole Z
étant un état final simple, c'est pourquoi il n'apparaît pas parmi les éléments de
VN.
Il convient à présent de montrer comment on pourrait construire un automate
d'états finis à partir d'une expression régulière. Il existe une méthode basée sur le
calcul des dérivées permettant d'obtenir systématiquement cette construction. A
ce titre, on va rappeler d'abord quelques propriétés fondamentales sur les
expressions régulières avec les dérivées.
Langages réguliers 57

4.5 Passage d'une expression régulière à l'automate fini correspondant


On note l'opérateur de la dérivée d'un langage par le symbole //, et on note R,
l'expression régulière qui dénote le langage L.

Dérivée d'un langage


La dérivée d'un langage L par rapport au mot ro est égale à l'ensemble
D ={a. E VT * 1 a. (l) E L}.

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.

Construction de l'automate fini par la méthode des dérivées


Par exemple, on considère l'expression régulière R = c (a+® c)*.
On pose R = sa état initial de l'automate.
En se basant sur les propriétés fondamentales citées ci-dessus, on construit
progressivement l'automate fini pour R.
On démarre avec R = sa, en calculant sa dérivée par rapport à 11 a 11 , puis par
rapport à 11 c 11 • Ensuite, pour chaque dérivée calculée, on calcule la dérivée par
rapport à 11 a 11 et à 11 c 11 , et ainsi de suite, jusqu'à ne plus pouvoir rencontrer de
nouvelles dérivées. On suit alors les étapes suivantes, en respectant les propriétés
fondamentales rappelées ci-dessus :
sa// a = c (a+ ® c ) *//a = 0
sa//c = c(a+ ® c)*//c = (a+ ® c)* = s1 E F (car si contient e), alors
I(so, c) =
s1.
On continue les calculs avec si= (a+® c)*, ensuite on procède de la même
façon pour chaque nouvelle dérivée calculée.
58 Chapitre 2

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.

En récapitulant tous les calculs effectués, on obtient l'automate


A = (S, sa, VT, F, I) tel que :
S = {sa, si, s2, s3}
sa est l'état initial
VT ={a, c}
F = {s1, s2, s3}
La fonction de transition 1 est
I(sa, c) = s1 e F
I(s1, a) = s2 e F
I(si, c) = s3 e F
I(s2, a) = s2
I(s2, c) = s3
I(s3, a) = s2
I(s3, c) = s3
Le diagramme de transition associé est illustré par la Figure 34.

Figure 34 : Diagramme de transition de l'expression régulière c (a+ Ef> c)*


Langages réguliers 59

Méthode intuitive de passage de l'expression régulière à l'automate fini


Avec la méthode des dérivées on obtient un automate déterministe. On peut
construire le même automate que le précédent ou un autre équivalent en utilisant
la méthode intuitive basée sur les diagrammes de transition. On reconsidère alors
la même expression régulière R = c (a+ © c)*. On peut découper R en plusieurs
sous-expressions régulières, ensuite on construit pour chacune d'elles l'automate
correspondant. A la fin du processus, on rassemble ses automates de manière à
retrouver l'automate correspondant à R.
Initialement on peut par exemple découper R en les sous-expressions c et
(a+© c)*. Ensuite, c'est la sous-expression (a+© c)* qui sera à son tour découpée
en a+ et c, et ainsi de suite, en appliquant progressivement le processus jusqu'à
obtenir l'automate correspondant à R.
Ainsi, les sous-expressions c et a+ sont représentées respectivement par les
diagrammes (i) et (ii) de la Figure 35.

(i) (ii)

(iii) (iv)

(v) (vi)

Figure 35: Diagrammes des sous-expressions de l'expression c (a+© c)*

Le diagramme (iii) de la sous-expression (a+ © c) est obtenu en réunissant les


diagrammes (i) et (ii). On rajoute les transitions spontanées I(f, E) = s et
l(g, E) = s au diagramme (iii) qui produit le diagramme (iv). Ce dernier à son
tour est transformé pour donner le diagramme (v) qui représente la sous-
60 Chapitre 2

expression (a+ Etl c) *. La transformation en soi consiste tout simplement à éliminer


les e-transitions du diagramme (iv). Enfin, en concaténant le diagramme (v) à (i)
et en renommant leurs états, on obtient le diagramme (vi) qui représente
l'expression régulière R complète. Pour confirmation, il suffit de voir que le
diagramme (vi) est exactement le même que celui de la Figure 34.

Il existe d'autres méthodes de construction d'automates d'états finis à partir


d'expressions régulières. On cite particulièrement la technique de construction de
Thompson qui produit un automate fini non déterministe, mais que l'on peut
transformer, au besoin, en automate fini déterministe. On peut citer à titre
indicatif également une autre technique qui construit un automate d'états finis
directement en s'appuyant sur l'arbre abstrait de l'expression régulière en
question. Cette technique est décrite en détail au chapitre 5 dans le cadre de la
génération automatique d'analyseurs lexicaux.

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

g• / les nombres réels avec ou sans exposant :


1er cas : qui peuvent commencer par un chiffre ou un signe ( + ou - ) et
n'acceptant pas la virgule décimale « . » en début du nombre. Le point ne peut
apparaitre qu'au milieu ou en fin du nombre. Ces nombres réels sont les mêmes
que l'on rencontre par exemple en Borland Pascal.
2eme cas : qui peuvent commencer par un chiffre ou un signe ( + ou - ) et qui
peuvent admettre la virgule décimale « . » même en début du nombre.

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

Après la suppression des e-transitions, on obtient le diagramme suivant :

Ce diagramme permet d'avoir la grammaire régulière à droite suivante :


62 Chapitre 2

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.

3° / Entiers naturels pairs ou entiers naturels divisibles par 2 :


s~cslp
Les chiffres c et p sont tels que c E {O, 1, 2 ... 9} et p E {O, 2, 4, 6, 8}

4°/ Entiers naturels divisibles par 3 :


Avec les nombres entiers naturels divisibles par 3 on raisonne en termes de classes
d'équivalence. On a 3 classes qui sont représentées respectivement par les restes
de la division par 3 (0, 1 et 2).
Les nombres 0, 3, 6, et 9 sont évidemment divisibles par 3. Mais, parmi les
nombres divisibles par 3 supérieurs à 10, il y en a qui commencent par 1, 2, 4, S, 7
et 8. A titre indicatif, si un nombre commence par le chiffre 1, pour qu'il soit
divisible par 3, on doit concaténer à ce 1, un nombre qui produit un reste = 2
lorsqu'il est divisé par 3. De même, si un nombre commence par le chiffre 2, on
doit lui concaténer un nombre qui produit un reste = 1 lorsqu'il est divisé par 3.
Ainsi, on a la grammaire régulière à droite qui engendre les nombres entiers
divisibles par 3 qui est représentée par les règles suivantes :
S ~ 0 13 16 19 1OS 13S 16S 19S l lA 14A 17A 12B 1SB 18B
A ~ 2 I s I 8 1OA 13A 16A 19A l rn 14B 17B 12S 1SS 18S
B ~ 1 I 4 I 7 l lS 14S 17S 1OB 13B 16B 19B 12A 1SA 18A
Intuitivement, pour un seul chiffre on a: 0, 3, 6, 9 (avec les règles S ~ 01 3 I
6 l 9). On peut continuer récursivement sur S avec S ~ 0 l 3 l 6 l 9 I OS l 3S l 6S l 9S,
mais, on n'aura que les entiers naturels divisibles par 3 qui sont formés par les
chiffres 0, 3, 6, 9. Pour faire participer les autres chiffres, à savoir, {1, 4, 7} et {2,
S, 8}, on doit former d'autres compositions avec les règles S ~ lA 14A 1 7A et S
~ 2B 1 SB 18B. On doit faire en sorte que si on utilise S ~ lA 1 4A 1 7A ou S ~
Langages réguliers 63

2B 1 5B 1 8B, on doit nécessairement ajuster avec les règles A ~ 2 I 5 1 8, pour


aboutir aux combinaisons à 2 chiffres : 12, 15, 18, 42, 45, 48, 72, 75, 78. Même
raisonnement avec les règles B ~ 1 I 4 I 7, pour obtenir les combinaisons : 21, 24,
27, 51, 54, 57, 81, 84, 87. On tient le même raisonnement pour les combinaisons
de nombres entiers naturels de longueur > 2. Les 40 règles de la grammaire
engendrent le langage des entiers naturels divisibles par 3. S représente l'axiome.
On peut revenir à la question des 3 classes d'équivalence évoquées (reste = 0, 1 et
2). En effet, si l'axiome est représenté par A au lieu de S, il n'est pas difficile de
déduire que le langage engendré correspond à celui des nombres entiers naturels
qui produisent un reste = 2, quand ils sont divisés par 3. Il en est de même, en ce
qui concerne les nombres qui produisent un reste = 1 quand ils sont divisés par
3 ; c'est le cas quand l'axiome est représenté par B au lieu de S.
5° / Les nombres binaires divisibles par 2 sont les nombres binaires qui se
terminent toujours par O. Leur grammaire génératrice régulière à droite est décrite
par les règles suivantes :
S ~OS l lS 1 0
où S est l'axiome, et il est l'unique élément de VN.
6° / La grammaire régulière à droite qui engendre les nombres binaires ayant un
nombre de 0 divisible par 3 est décrite par les règles suivantes :
S ~
OA l lS 1 E
A~ lA 1 OB
B ~OS l 1B
Quand ce n'est pas très évident, il serait judicieux d'utiliser le diagramme de
transition d'un automate fini pour mieux voir la propriété de divisibilité par 3 du
nombre d'occurrences du caractère O.

7° / La grammaire régulière à droite qui engendre des chaines sur l'alphabet


{a, b, c}, dont le dernier caractère est apparu au moins une fois, est représentée
par les règles :
S ~ aS 1 bS 1 cS 1 aA 1 bB 1 cC
A ~ aA 1 bA 1 cA 1 a
B ~ aB 1 bB 1 cB 1 b
C ~ aC 1 bC 1 cC 1 c
Tout comme la question précédente, on peut utiliser le diagramme de
transition d'un automate fini équivalent à cette grammaire pour s'assurer du
respect de la propriété d'apparition du dernier caractère au moins une fois.
64 Chapitre 2

a, b, c a

a, b, c

a, b, c

8° / Les nombres entiers naturels divisibles par 5 se terminent par 0 ou 5, comme


0, 5, 10, 15, etc. Il suffit de trouver des règles permettant de générer des nombres
sur l'alphabet VT = {O, 1, ... , 9} qui se terminent par 0 ou 5.
La grammaire régulière à droite qui engendre ces nombres est décrite par les
règles :
S ~ 0 1 5 1 aS ; avec a E VT·

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

A titre d'exercice de perfectionnement le lecteur est invité à retrouver lui-


même les automates finis équivalents aux deux cas de grammaires trouvées ci-
dessus.
On peut également trouver intuitivement la grammaire régulière à gauche pour
chacun des cas précédents. Par exemple, pour les nombres binaires pairs, on a les
règles S -7 AO 1 0 et A -7 AO 1 Al 1 0 1 1. On peut laisser les autres cas à titre
d'exercices pour le lecteur. Pour rappel, il existe également des procédures
permettant de construire systématiquement une grammaire régulière à gauche à
partir de son équivalente régulière à droite et vice versa. Ses procédures
nécessitent au préalable d'apprendre à construire un automate d'états finis à
partir d'une grammaire régulière et inversement.

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> a(b~a) *b) / / ! = ( ( a<f>b): / / a) ba(b<f>a) *b <f> ba(b<f>a) *b / / a


<f> a(b<f>a) b //a= (a<f>b) ba(b<f>a) b <f> (b<f>a) b = S3, alors 1(52, a) = 83.
((a<f>b)*ba(b<f>a)*b <f> a(b<f>a)*b) // b = ((a<f>b)* // b)ba(b<f>a)*b <f> ba(b<f>a)*b // b
<f> a(b<f>a)*b//b =(a<f>b)*ba(b<f>a)*b <f> a(b<f>afb = s2, alors 1(52, 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

((a®b) *ba(b®a) *b ® (b®a) *b) l lb= (a®b) *ba(b®a) *b ® a(b®a) *b ® (b®a) *b ® E


= s4, alors 1(53, b)= 84 1 l'état S4 est un état final.
((a®b~ *ba(b®a) *b• ® a(b®a).*b ® (b~a) *b ® E•) 11 a = (a®b) *ba(b®a) *b ®
(b®a) b ® (b®a) b = (a®b) ba(b®a) b ® (b®a) b = s3; alors 1(54, a) = 83.

((a®b) *ba(b®a) *b ® a(b®a) *b ® (b®a) *b ® E ) 11 b (a®b) •ba(b®a) •b ®


a(b®a) •b ® (b®a) •b ® E = s4; alors 1(54, b) = 84.

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é.

2° I Construction intuitive selon les diagrammes de transition.


On peut adopter la stratégie qui consiste à découper l'expression régulière
a(a®b)*ba(b®a)*b, en plusieurs sous-expressions, à savoir, a, (a®b)*, ba et b. On
concatène, ensuite, ces dernières dans l'ordre, de sorte à obtenir le diagramme de
transition de toute l'expression. Le diagramme ci-après est une illustration de
cette stratégie. Cependant, l'automate correspondant n'est pas déterministe.

Il suffit de le rendre déterministe ; ce qui est illustré schématiquement ci-après :

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•.

3• / La déduction de la grammaire régulière à droite est immédiate. On remplace


les états sO, sl, s2, s3 et s4, respectivement par S, A, B, C et D. L'état final s4,
n'est pas un état final simple, par conséquent, s4 correspond à son homologue D
dans VN.
La grammaire régulière à droite est décrite par les règles suivantes :
S ~ aA; A~ aA 1bB; B ~ aC 1bB; C ~ aC 1bD1b;D~aC1bD1 b;

La grammaire régulière à gauche peut être déduite intuitivement, ou bien elle


peut être construite en passant par l'automate fini par application de la procédure
habituelle de la section 4 de ce chapitre.
Intuitivement, d'après l'expression régulière a(a®b) *ba(b®a) *b, on obtient
directement la grammaire dont les règles de production sont les suivantes :
S ~ Ab; A ~ Aa 1Ab 1Ba; B ~ Cb; C ~ Ca 1Cb 1a.

On peut aussi utiliser l'automate précédent comme suit :

b
=>

qui produit l'automate miroir représenté par le diagramme suivant :

<==

La grammaire régulière à droite extraite à partir de ce diagramme est décrite


par les règles suivantes :
S ~ bA 1bB
A~ Ba lbB
B ~ aA 1aB 1aC
C ~bC lbD
D~aD la
68 Chapitre 2

La grammaire régulière à gauche recherchée est décrite par les


règles suivantes :
S ~Ab 1Bb
A~ Ab 1Bb
B ~ Aa 1Ba1 Ca
C ~ Cb 1Db
D ~Da la
En supprimant S, car il est superflu, on obtient la grammaire suivante :
A~ Ab IBb
B ~ Aa 1Ba1 Ca
C ~ Cb 1Db
D ~Da 1a
Cette grammaire est équivalente à la grammaire trouvée intuitivement ci-
dessus, à savoir, celle décrite par les règles suivantes :
S ~ Ab; A ~ Aa 1Ab 1Ba; B ~ Cb; C ~ Ca 1Cb 1a
Il y a plusieurs manières de prouver cette équivalence. On se limite ici à la
technique de l'automate fini.
En effet, on peut construire directement l'automate d'états finis de chacune des
deux grammaires, ensuite on se ramène aux automates finis déterministes
correspondants. Si ces derniers sont identiques, on s'arrête, et leur équivalence est
prouvée, sinon on les minimise. Après l'étape de minimisation, on peut vérifier
encore s'ils sont identiques, auquel cas, on s'arrête, car on a prouvé que les deux
grammaires sont effectivement équivalentes, sinon, au moins l'une des deux
grammaires est fausse.
Si l'on utilise les règles :
A~ Ab 1Bb
B ~ Aa 1Ba1 Ca
C ~ Cb 1Db
D ~Da 1a
on déduit directement, à partir de la grammaire régulière à gauche, les transitions
suivantes:
I(A, b) =A / * A est l'état final de l'automate * /
I(B, b) =A
I(A, a)= B
I(B, a)= B
I(C, a)= B
I(C, b) = C
I(D, b) = C
I(D, a)= D
I(E, a)= D / * E est l'état initial de l'automate ; il n'apparait pas parmi
les règles * /
Ces transitions s'expriment également graphiquement par le diagramme de
transition suivant :
Langages réguliers 69

Comme on peut le constater, cet automate est déterministe, et il est identique


à celui qu'on a construit précédemment par la méthode des dérivées.
On considère maintenant les règles de la grammaire régulière à gauche trouvée
intuitivement : S ~ Ab; A ~ Aa 1 Ab 1 Ba; B ~ Cb; C ~ Ca 1 Cb 1 a
Les transitions correspondantes sont les suivantes :
I(A, b) = S / * S est l'état final de l'automate* /
I(A, b) =A
I(A, a)= A
I(B, a)= A
I(C, b) = B
I(C, b) = C
I(C, a)= C
I(D, a) = C / * D est l'état initial de l'automate ; il n'apparait pas parmi
les règles * /

Ces transitions s'expriment graphiquement par le diagramme ci-après :

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

Cet automate n'est pas déterministe. On doit le transformer en automate


déterministe pour achever la démonstration (voir tabes précédentes).
La matrice de transition est donnée par les tables précédentes.
On construit le diagramme de transition de l'automate sur la base des transitions
de la dernière table.

b
=>

Ce diagramme est identique à celui qu'on a construit par la méthode des


dérivées. Autrement dit, les deux grammaires régulières à gauche, à savoir, celle
construite intuitivement et celle obtenue en passant par l'automate sont
équivalentes. C.Q.F.D.

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;

{Onln 1 n 2 1} 11 0 = {1, 01 2, 0213, ... } = {On-lln 1 n 2 1} = s1 ;


{Onln ln2l}ll1=0;

{on-lln 1 n 2 1} 11 0 = {1, 01 2, 0213, ... } 11 0 = {0, 12, 01 3, 0214, ... }


{on- 1n 1 n 2 2} = s2 ;
{On-lln 1n2 1} Il 1 = {1, 01 2, 0213, ... }Il 1 = {e} = s3;

{On- 21n ln 2 1} = s2 Il 0 = {On-31n 1n2 3} = s4;


{On- 21n 1n21} = s2 Il 1 = {1};
Alors 1 11 aï= {on-iln 1 n ~ i > O} avec n ~ CX> et i aussi. Donc, le nombre de
dérivées tend lui aussi vers l'infini, et par conséquent, 1 1 (1: type 3.

2°/ Démontrer que 12={anbm1 m >net n ~ O} n'est pas de type 3.


On se base sur le même raisonnement que précédemment, à savoir, on calcule des
dérivées pour montrer que 1 2 n'est pas régulier.
Langages réguliers 71

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

L'automate complémentaire est décrit par le diagramme de transition suivant.


On note l'état erreur par le symbole R.

Ce qui donne la grammaire régulière suivante :


A ~ c 1 cB 1 a 1 aD 1 b 1 bR 1 e /*A est l'axiome */
B ~ c 1 cB 1 a 1 aC 1 b 1 bR
C ~ a 1 aR 1 c 1 cR 1 bF
D ~ a 1 aR 1 c 1 cR 1 b 1 bE
E ~ cF 1 a 1 aR 1 b 1 bR
F ~ a 1 b 1 c 1 aR 1 bR 1 cR
R ~ a 1 b 1 c 1 aR 1 bR 1 cR

On a choisi la grammaire régulière à droite parce qu'habituellement elle est


plus facile à extraire directement à partir du diagramme de transition. Le lecteur
peut, à titre d'exercice, extraire la grammaire régulière à gauche équivalente. On
peut s'inspirer de la solution de la question 3° de l'exercice 5.2. On peut également
utiliser la technique basée sur l'automate miroir (voir section 4 de ce chapitre ou
les exercices précédents).

Automate d'états finis et grammaire régulière pour le langage défini par la


réunion des langages Ll et L2 : (LluL2).
Langages réguliers 73

On construit le diagramme de transition pour chacun des langages Ll et L2,


ensuite, on assemble les deux diagrammes selon l'opération de réunion. Outre
l'automate, le diagramme résultant permet aussi d'avoir la grammaire régulière
pour le langage LluL2.
a

=>

La grammaire régulière équivalente est décrite par les règles suivantes :


S ~ aA 1aB1 cC
A~aAlc
B~bD
D~c
C ~ cC 1 aE
E~b

Pour le langage Ll *, on considère le diagramme de transition de l'automate fini


qui reconnait Ll, que l'on transforme comme c'est illustré schématiquement
par la figure suivante :

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

La grammaire régulière à droite extraite directement de l'automate miroir est


définie par les règles suivantes :
Z ~ lA j 1 j OC j ë
Langages réguliers 75

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

2°/ Méthode basée sur le lemme d'Arden


A~ a. I ~A; solution A= ~*a.
On pose: A~ OB l lA 1 e (1)
B ~OC l 1B (2)
C ~ OA l lC (3)
A est l'axiome.
(1) implique que A= l*(oB ® e)
(2) implique que B = 1*oc

On remplace A par sa valeur dans (3), on obtient: C ~ 01*(0BEB e) l lC: (4). En


remplaçant B par sa valeur dans (4) on aura: C~Ol *(OB®e) l lC: (4) qui. donne
l'équation suivante :
C ~ 01* 1 (1®01*01*0) C, dont la solution est C = (1 ® 01*01*0)* 01*
B = l*oc = 1*0(1 œ 01*01*0)* 01*
A= 1*(01*0(1 ® 01*01*0)* 01* ® e)

3°I Automate fini déterministe correspondant :


On utilise la méthode des dérivées :

A 11 o = ! *(01 *0(1*®01 *01 *p) **01**®ejl IO * * * * *


= 1 l IO. (01 0(1®01 01 0) 01 ®e) ® 01 0(1®01 01 O) 01 ) 11 O ® e 11 O
= 0 ® 1*0(1®01 *01 *o)* 01 * ® 0 = B, alors l(A, 0) = 8
A Il 1=1*(01*0(1®01*01*0)* 01*œe) Il 1
= 1*l IL(Ol *0(1®01 *01* *®e) ® 01 *0(1®01 *
O)*01 01*0)*01
* ®e) 11 1
* * * * * *
= 1 (01 0(1®01 01 O) 01 ®e) ® 0 ® 0
= A, alors l(A, 1) = A, et comme A contient e, donc A est final.
B Il 0 = 1*0(1®01*01*0)* 01* Il 0
=l *l IO. 0(1®01 *
01* * ® 0(1®01 *
0)*01 01*0)*01
* I IO
= * * * *
0 ® (1®01 01 0) 01 = C, alors 1(8, 0) = C
B Il 1=1*0(1®01*01*0)* 01* Il 1
= i:11i. o(~EB~1*~1*of 01* EB 0(1®01*01*0)* 01*œ e) 111
= 1 0(1®01 01 0) 01 ® 0 = B, alors 1(8, 1) = 8
76 Chapitre 2

c 11 o = (1®01 *01 *o) * 01 * 11 o


= (1®01 *01 *0) * II o. 01 * ® 01 * 11 0
= (1®01 *01 *o) 11 0(1®01 *01 *o) *01 * ® 1
= f0® 1*01*0) (1®01*01*0)*01*® 1*
'li * * * * *
= 1 {01 0{1®01 01 0) 01 ®E) = A, alors I(C, 0) = A
c 11 1 = (1®01 *01 *o) * 01 * 11 1
* **o) *11i. 01 * ~ ~1 *J1 ~
= c1®01*01
= (1®01 01 0)/ IL(l®Ol 01 0) 01 ® 0
= (1®01 *01 *0) * 01 * = C, alors I(C, 1) = C
En récapitulant, on obtient le même diagramme de transition que celui issu de la
grammaire régulière donnée par hypothèse. Le diagramme est illustré par le
graphe suivant :

4° I L'automate est-il minimal ?


En appliquant l'algorithme de minimisation on s'aperçoit que:
il n'y a pas d'états parasites ou superflus ;
l'automate est déterministe.
Pour =.0 on a deux grandes classes {A} et {B, C}
Ensuite, I(B, 0) = C ; I(C, 0) = A ; I(B, 1) = B ; I(C, 1) = C. Mais, si B =.° C,
on n'a pas en même temps 1 (B, 0) =.0 I(C, 0), car C n'est pas =.0 A. Par
conséquent, {B, C} éclate en deux classes {B} et {C}.
On s'arrête, car chaque classe est formée d'un seul état qui est indivisible.
L'automate est donc minimal.
La grammaire régulière à droite trouvée est celle donnée par hypothèse. Ceci
confirme que tous les calculs ainsi effectués sont corrects.
A~ OB l lA le
B ~OC l lB
C ~ OA l lC
Chapitre 3
Grammaires hors contexte et
automates à pile

Les modèles les plus largement employés pour la représentation des


langages de type 2 sont les grammaires hors contexte et les automates à pile.
On peut tout aussi représenter ces langages au moyen de diagrammes
syntaxiques et/ou de diagrammes de transition. On verra tout au long de ce
chapitre qu'il existe des variantes de modèles de représentation. Les règles de
production se présentent comme la notation la plus concise et la plus
générale. Mais, parfois pour des besoins spécifiques, on préfère utiliser les
diagrammes syntaxiques. Souvent, pour des raisons techniques, comme
l'optimisation par exemple, on utilise les diagrammes de transition
d'automates à pile pour mieux expliciter les schémas d'algorithmes
d'analyseurs.

1 Grammaires de type 2 et leurs différentes notations


Définition 1.1 (Grammaire hors contexte ou de type 2)
Une Grammaire G = (VN, VT, S, P) est de type 2, si toutes ses règles de
production sont de la forme: A ~ ~. avec A E VN et ~ E V • sachant que
V= VTUVN.

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.

Définition 1.2 (notation BNF et EBNF)


Une notation normalisée dite aussi BNF (Backus-Naur Form}, initialement
appelée forme normale de Backus, consiste à exprimer les règles de
production en utilisant les symboles suivants :
• La flèche est remplacée par le symbole« ::= ».
• Les non-terminaux sont encadrés par les chevrons « < » et « > »,
également appelés chevrons obliques.
• La barre verticale « 1 » représente le choix ou l'alternative.
Par exemple, les règles de production suivantes :
S ~A* S
S~A
A~B+A
A~B
B ~ (S)
B~a

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

Il faut noter qu'à l'origine, la notation conventionnelle des règles de production


ne comportait pas de barre verticale « 1 ». Autrement dit, chaque règle de
production est écrite sur une ligne séparée (pour une meilleure lisibilité). La barre
verticale (empruntée à la BNF), a été ajoutée à la notation conventionnelle par
plusieurs auteurs afin d'optimiser l'écriture de la grammaire.
Les règles de la grammaire ci-dessus peuvent être écrites sur trois lignes au lieu
de six. Ainsi, même avec la notation conventionnelle, au lieu d'écrire, par
exemple, les règles S ~ A * S et S ~ A, sur deux lignes séparées, on peut les
écrire sur une seule ligne. On aura donc, S ~ A * S 1 A.
Par ailleurs, quand les chevrons « < » et « > » sont utilisés pour concevoir
des non-terminaux, il devient possible pour un non-terminal (représenté
d'ordinaire par une seule lettre majuscule dans la notation conventionnelle), d'être
représenté par un mot (ou chaine de caractères). Cette option permet aux
symboles non-terminaux d'être mnémoniques et significatifs.
Les règles de production de la grammaire donnée en exemple précédemment,
peuvent être réécrites sous forme BNF avec de nouveaux symboles non-terminaux
représentés par des chaines de caractères. En effet, les règles de production écrites
avec la notation conventionnelle comme suit :
S ~ E 1 +E 1-E
E~TIE+FI E-T
T~ F 1 T * F 1 T / F
F ~ i 1n1 (S)
peuvent être remplacées, dans la notation BNF, par des règles comportant des
symboles non-terminaux mnémoniques et significatifs, comme illustré par les
productions suivantes :
<Sentence> :: = <Expr> 1 + <Expr> 1 - <Expr>
<Expr> ::= <Terme> 1 <Expr> + <Terme> 1 <Expr> - <Terme>
<Terme> ::= <Facteur> 1 <Terme> * <Facteur> 1 <Terme> / <Facteur>
<Facteur> ::= i 1 n 1 (<Sentence>)

Il est possible d'étendre aussi la BNF en utilisant un formalisme emprunté des


expressions régulières. On obtient ainsi l'EBNF (Extended Backus Naur Form) ou
l'extension de la forme normale de Backus. Pour cela, on introduit les notations
suivantes :
{ }• qui dénote la répétition 0 ou plusieurs fois.
{ } + qui dénote la répétition 1 ou plusieurs fois.
{ } *k qui dénote la répétition (bornée supérieurement par k) au plus k fois.
{ } +k qui dénoté la répétition au moins 1 fois et au plus k fois.
? qui dénote l'option (0 ou 1) fois.
A noter que dans certains ouvrages, on rencontre aussi une notation équivalente
qui utilise les accolades « { } ». Par exemple, les règles S ~ A 8 B 1 A, peuvent
être regroupées, par factorisation, en une seule règle condensée S ~ A {SB}. Le
80 Chapitre 3

symbole « { } » est en quelque sorte interprété comme à { }* 1 , c'est-à-dire { }*\


avec k = 1.
On rencontre également le symbole « [ J » qui permet la factorisation. La règle
S ~A {8B}, peut alors être réécrite sous la forme S ~A [8B 1 E].
La grammaire, sous forme BNF, de l'exemple précédent, représentée par les
règles :
<S> ::=<A>* <S> 1 <A>
<A> ::= <B> + <A> 1 <B>
<B> ::= (<S>) 1 a
peut donc être réécrite sous forme EBFN comme suit:
<S> ::= <A> {*<A>} *
<A> ::= <B>{ + <B>} *
<B> ::= ( <A>{*<A>} * ) 1 a
Il est facile de montrer comment on a obtenu ce résultat. Il suffit de se référer
à la notion de dérivation introduite au chapitre 1.
En effet, comme <S> ::= <A> * <S> 1 <A>, c'est-à-dire S ~ A * S {l) et
S ~A (2l, alors en appliquant la règle {ll, un certain nombre de fois, la dérivation
produit la séquence S ~A* S ~A* A* S... ~ A* A* A* ... * S, qui est égale à
{A*}+ S. Avec la règle (2) on a S ~ A. On a donc, S = {A*}+ A ou S = A.
Autrement dit, S ={A*}+ A Œ> A, c'est-dire S = ({A*}+Œ> E) A= {A*}* A=
A*A* ... *A*A = A{*Af. Ceci confirme le résultat escompté, à savoir,
<S> ::= <A>{*<A>} *. Le même résultat aurait été obtenu en appliquant
directement le lemme d'Arden vu en section 3 du chapitre 2. En effet, <S> ::=
<A> * <S> <A> (ayant pour équation équivalente <S> ::= <A> * <S> Œ>
1

<A>), a pour solution <S> = (<A>*) *<A> = {<A>*} *<A>


<A>{*<A>} *qui est conforme au résultat envisagé.
On obtient <A> = <B> { <B>+} *pour les règles <A> ::= <B> + <A> 1 <B>,
en adaptant le même raisonnement. Quant aux deux dernières règles, <B> ::=
( <S>) 1 a, une simple substitution de <S> par sa valeur (déjà calculée
précédemment), confirme la véracité du résultat envisagé, à savoir, <B> ::=
( <A>{*<A>} * ) 1 a.
Remarque 1.3
Afin de simplifier l'écriture, on effectuera un léger changement de notation sur le
formalisme conventionnel des règles de production en empruntant celui de
l'EBNF. Ainsi, la grammaire ci-dessus définie par les règles :
<S> ::= <A>{*<A>} *
<A> ::= <B>{ + <B>} *
<B> ::= ( <A>{*<A> }*) 1 a
sera une combinaison de la forme conventionnelle et de l'EBNF, écrite de la
manière suivante :
S ~ A{*Af
A~ B{+ B} *
B ~ (A{*A>}*) 1 a
Grammaires hors contexte et automates à pile 81

1.1 Diagramme syntaxique


Le diagramme syntaxique est une notation très répandue, que l'on rencontre dans
divers manuels pratiques ou théoriques sur les langages de programmation. C'est
un formalisme très pédagogique permettant d'exprimer graphiquement les règles
de production d'une grammaire, et donc de décrire n'importe quel langage ou
grammaire hors contexte.
Les diagrammes syntaxiques donnent un moyen souvent plus intuitif de
percevoir la notion de grammaire que les représentations textuelles, qui elles, sont
souvent mieux adaptées aux outils d'analyse syntaxique qui servent à la
construction de compilateurs.

Pour établir l'analogie entre les diagrammes syntaxiques et les grammaires, il


convient d'abord de définir un certain formalisme. Il s'agit alors d'exprimer les
règles de production en utilisant les éléments suivants :

Un cercle (ou ellipse) annoté(e) par un élément de l'alphabet terminal (VT)


correspond à un symbole terminal de V T·
La forme rectangulaire (ou carrée) annotée par un élément de l'alphabet non-
terminal VN, désigne un symbole non-terminal de l'ensemble VN.
Ainsi, pour construire le(s) diagramme(s) syntaxique(s), il y aura :
Autant d'éléments de VN que de diagrammes syntaxiques à construire.
Autant de rectangles dans un diagramme que d'éléments de VN apparaissant
dans le membre droit d'une règle de production.
Autant de cercles (ou ellipses) que d'éléments de VT apparaissant dans le
membre droit d'une règle de production.
L'entrée (Input) de chaque diagramme est marquée par le symbole non-
terminal correspondant.
La sortie (Output) d'un diagramme correspond au(x) membre(s) droit(s) de la
(des) règle(s) de production concernant le symbole non-terminal présenté en
entrée (Input) du diagramme.
La liaison entre les différents éléments d'un diagramme est assurée par des
connecteurs avec flèches à la fin.
La série d'exemples suivante illustre l'équivalence entre les règles de
production et les diagrammes syntaxiques correspondants.

Les règles de production A ~ a 1 B sont représentées par le diagramme de la


Figure 36.

Les règles de production A ~ c 1 abBA sont représentées par le diagramme de


la Figure 37.

Les règles de production A ~ abB 1 abBA sont représentées par le diagramme


de la Figure 38.
82 Chapitre 3

Input A ~ Output

Figure 36 : Diagramme syntaxique des productions A ~ a 1B

Input A Output

Figure 37 : Diagramme syntaxique des productions A ~ c 1 abBA

Output

Figure 38 : Diagramme syntaxique des productions A~ abB 1 abBA

1.2 Graphe syntaxique


Les graphes syntaxiques sont un autre moyen de représenter graphiquement des
grammaires hors contexte. Ils constituent une alternative à la représentation basée
sur le diagramme syntaxique décrit ci-dessus. La conception du graphe est inspirée
de la notion de liste chainée.
Ainsi, en considérant une grammaire G = (VN, VT, S, P), le graphe
syntaxique correspondant (équivalent) est construit compte tenu des conditions
suivantes:
Pour chaque élément de V N, est associée une liste chaînée.
Les règles de production sont exprimées par les éléments de listes et leurs
chaînages.
Chaque élément de liste est constitué de 3 champs C 1, C2 et C3 .
C1 admet une des 3 valeurs (étiquettes) T, N ou A.
• T indique que l'élément de la liste dénote un terminal E VT.
• N indique que l'élément de la liste dénoté est un non-terminal E VN.
• A indique si le membre droit de la règle de production en cours possède
une alternative.
C2 est, selon la valeur de C1,
• soit un terminal E VT (C1 = T).
• soit un pointeur vers un élément non-terminal E VN (C 1 = N).
Grammaires hors contexte et automates à pile 83

• soit un pointeur vers le 1er élément du membre droit de la règle alternative.


L'élément en question peut être un terminal ou un non-terminal ; dans les
deux cas, C1 = A.
C3 est un pointeur vers un élément qui représente le prochain terminal ou non-
terminal dans le membre droit de la règle de production en cours (lorsque
C1 = N ou T, C3 peut être = Nil). Lorsque le pointeur C3 prend la valeur Nil
ou « / », il marque la fin d'une production. Lorsque C1 = A, C3 ne peut pas
être= Nil.
Le graphe syntaxique de la grammaire définie par les règles suivantes :
S-7 B!S+B
B-7C!B*C
C -7 a! (S)
est donné par la liste chainée de la Figure 39.

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

# E rest l'élément de fond de pile dit aussi symbole initial de pile,


F i;;;;; S est l'ensemble fini des états finals,
I : Sx(VTU{ e} )xr~ f<J (Sxr *)est la fonction de transition.
Schématiquement un automate à pile peut être vu comme une machine abstraite
muni des différents organes d'un reconnaisseur, comme illustré par la Figure 40.

~ Tête de lecture
1 1 l l±J[ 1 rn+ ---- Bande de lecture

1
! sommet -- ..
Bloc de contrôle 1
B }
B + - - - Pile

Figure 40 : Machine abstraite représentant un automate à pile

Contrairement à l'automate d'états finis (automate sans pile), l'automate à pile


est muni d'une unité de mémorisation dans laquelle seront stockés des symboles
d'un alphabet particulier nommé alphabet de pile.
Ce type d'automate peut, par exemple, reconnaitre le langage des expressions
parenthésées, parce que la pile peut permettre à l'automate de se souvenir du
nombre de parenthèses ouvrantes déjà rencontrées, et donc de pouvoir contrôler
facilement le nombre de parenthèses fermantes correspondantes restantes.
Un exemple de langage reconnu par un tel automate est l'ensemble des chaines
anbn. En effet, l'automate est capable de reconnaitre les chaines de type anbn parce
que la pile permet de mémoriser les lettres 11 a 11 afin de pouvoir les faire
correspondre avec leurs homologues "b". Il y aura autant de lettres "a" que de
lettres "b" en correspondance dans un mot anbn, comme c'est le cas des
parenthèses ouvrantes et fermantes dans une expression.
Les transitions de l'automate à pile vont être déterminées par trois éléments
correspondant à :
l'état courant,
le symbole courant sous la tête de lecture,
le symbole apparaissant au sommet de pile.
Ainsi, la fonction de transition I(s, a, A) = (q, a.) signifie qu'à l'état « s » et avec
le symbole « A» au sommet de la pile, l'automate (par le biais de l'unité de
contrôle), lit le symbole « a », transite vers l'état suivant « q », et remplace le
symbole « A » par le mot a. E r *.
On remarque que la valeur de la fonction de transition consiste en la paire (q, a.) ;
ce qui n'est pas le cas de l'automate fini où cette valeur correspond seulement à
un état.
On distingue deux modes de transition possibles dans le cas de l'automate à
pile:
Grammaires hors contexte et automates à pile 85

Soit il y a le déplacement de la tête de lecture d'une case vers la droite, sauf si


la transition s'effectue sans lecture (cas d'une E-transition) où la tête de lecture
reste immobile même si l'automate change d'état.

Soit il y a le dépilement du symbole du sommet de pile et l'empilement d'un


mot E r *. Dans le cas particulier où le mot à empiler est E, l'opération
d'empilement n'aura aucun effet sur la pile, et il n'est donc pas nécessaire de
l'exécuter.
On distingue également différents modes d'acceptation d'un langage
hors contexte, par un automate à pile :
Par pile vide L (Ar) = {mEVT* 1 (so, ro, #) 1--* (s, E, ë)} avec s E r, et la
pile est vide.

Par état final L (At) = {ffiEVT* 1 (so, ro, #) 1--* (st, E, a)} avec St E F et
aE r *.

Généralisé L (Ag) = {roEVT* 1 (so, ro, #) 1--* (st, E, E)} avec Sf E F, et la


pile est vide.

2.1 Fonctionnement de l'automate


Pour définir de manière précise le comportement d'un tel automate, il convient
d'introduire la notion de configuration. Une configuration est une représentation
concise de l'automate dans une situation donnée. Elle est définie par le triplet
(s, 'JI, y), où :
s est l'état courant de l'unité de contrôle ;
'JI est la sous-chaine non encore lue ;
y est le contenu de la pile.
Ci-après, sont énumérées les différentes configurations possibles d'un automate à
pile :
La configuration générale (s, 'JI, y), où « s » est un état quelconque
appartenant à S, 'JI est une sous-chaîne courante E VT*, et y est le contenu de
la pile, c'est-à-dire un élément E r *.
La configuration initiale (so, ro, #), où sa est l'état initial appartenant à S, ro est
une chaîne à analyser, et # est le symbole du fond de pile.
La configuration de succès ou d'acceptation de la chaîne ro est représentée :
• soit par (st, E, a) [par état final] ; Sf E F et a E r *contenu de la pile en fin
d'analyse.
• soit par (s, E, ë) [par pile vide].
• soit par (st, E, ë) [par critère généralisé].
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, V (s E S, a E VTU{E}, A E r) si (p, a) E I(s, a, A), alors
( s, am, Ao) 1- (p, m, ao) pour p E s, a, o E r * et m E VT *.
86 Chapitre 3

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.

2.2 Différentes notations de l'automate à pile


Tout comme l'automate d'états finis, il existe également diverses représentations
pour un automate à pile. On peut citer :
La représentation fonctionnelle
I(s, a, X) = {(q, a.)}, avec s, q E S, a E VTu{e}, X E r et a. E * C'est la
r.
notation la plus largement utilisée par la majorité des auteurs.
L'instruction
saX ~ qa., avec s, q E s, a E VTU{E}, X E r et a. E r*. C'est un
formalisme très concis, mais de moins en moins utilisé aujourd'hui.
La représentation matricielle
Comme la fonction de transition est dénotée par I(s, a, X), on utilise alors une
matrice à trois dimensions (ligne, colonne, matrice). Ces dernières sont
représentées respectivement par a E VTU{E}, s E S et X E r. La valeur de
chaque case d'une matrice est, soit {(q, a.)}, soit 0 (en cas d'absence de
transition). En fixant l'indice X correspondant au sommet de pile, on obtient
une matrice (comme celle de l'automate d'états finis). Cependant, les valeurs
de cette dernière sont des couples de type (q, a.) E Sxr* OU des cases= 0.
A titre d'exemple, soit l'automate dont la fonction de transition est définie
comme suit:
1 (s, a, Y)= {(q, A); (p, E)}
1 (s, b, A)= (q, AY)
1 (s, a, a) = (p, E)
1 (q, b, A) = (s, Y)
On peut faire en sorte, par exemple, que l'indice qui contrôle la matrice de
transition soit l'alphabet de pile r. Ainsi, comme illustré par le Tableau XI,
Grammaires hors contexte et automates à pile 87

quand la matrice est repérée par le sommet de pile Y, on a la matrice (i). De


même, quand on fixe le sommet A on obtient la matrice (ii). Enfin, quand la
matrice est repérée par le sommet de pile « a», on obtient la matrice (iii).
Cette représentation est très claire, mais techniquement, elle présente
l'inconvénient d'occuper beaucoup d'espace, particulièrement à cause des
nombreuses cases = 0, mises en jeu. Il faut toutefois noter qu'elle peut
constituer une table d'analyse prédictive aussi intéressante que les tables que
l'on rencontre dans les méthodes d'analyse syntaxique. Il suffit d'associer
l'analyseur (le simulateur) adéquat pour son interprétation.
s p q

a {(q, A), (p, e)} 0 0


(i)
b 0 0 0

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_)

Tableau XI- Représentation matricielle d'un automate à pile

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.

Figure 41 : Graphiques d'automate fini (sans pile} et d'automate à pile


La nouveauté par rapport à l'automate sans pile concerne les actions sur la
pile. Les mouvements sur la pile sont formulés par la sous-expression A (a), où A
est le symbole du sommet de pile à l'entrée (parallèlement à la lecture du symbole
11 a 11 ), et a est le mot résultant du remplacement du sommet de pile A en sortie. Il
88 Chapitre 3

s'agit, en quelque sorte, de dépiler le symbole du sommet de pile A et d'empiler a.


à la place. Le résultat final est donc, la transition vers l'état « q » avec a.
représentant le mot apparaissant au sommet de pile.
La notation graphique possède certains avantages que l'on ne trouve pas tous
réunis avec les autres notations. Il s'agit de :
la clarté et la lisibilité. En effet, grâce aux sommets et aux arcs annotés du
diagramme, on lit avec une grande facilité les mouvements de l'automate (état
de départ, lecture du symbole qui force la transition, état d'arrivée, sommet de
pile à l'entrée, sommet de pile résultant en sortie).
la facilité de construction (conception). La construction du diagramme
représentant l'automate est guidée par l'intuition. Cette construction consiste,
en quelque sorte, en une interprétation graphique du langage accepté par
l'automate.
La facilité d'implémentation. Grâce à sa puissance expressive, le diagramme de
transition est un modèle facile à traduire en programme informatique dans un
langage de programmation donné.
En bref, la représentation graphique habituelle des états de l'automate fini est
reconduite pour l'automate à pile comme illustré par le diagramme de la
Figure 42 qui comporte, entre autres :
un état initial représenté par un cercle nommé « s », et préfixé par une double
flèche ;
un état final représenté par un double cercle concentrique nommé « f ».

Figure 42 : Mise en valeur graphique des états (initial et final) d'un


automate à pile

2.3 Construction de l'automate à pile


On peut construire un automate à pile :
Soit intuitivement (approche vivement recommandée).
Soit formellement en s'appuyant sur une grammaire à contexte libre.
Les différents exemples suivants donnent une idée précise sur cette
construction :
Construction de l'automate à pile vide AE qui accepte le langage
L (AE) = L ={an bn 1n~1}.

Comme préconisé, pour construire facilement cet automate, on s'appuie sur le


graphe qui est une approche guidée par l'intuition. L'automate en question est
représenté par le graphe de la Figure 43.
Grammaires hors contexte et automates à pile 89

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}

L'automate Ae = (S, sa, VT, r, #, F, I) est effectivement, comme envisagé, un


automate à pile vide avec :
S = {sa, si, s2} qui représente l'ensemble des états de l'automate.
L'état sa est l'état initial de l'automate.
VT ={a, b} est l'alphabet terminal.
Le symbole# représente le symbole initial de la pile (fond de la pile).
r = {#}est l'alphabet auxiliaire de la pile composé d'un seul symbole #.
F = 0, autrement dit, le mode pile vide suffit pour reconnaitre le langage
L = {anbn 1 n~ l}.
La fonction de transition, conformément au graphe de la Figure 43, est définie
par:
1 (sa, a, #) = ( s1, # )
1 (s1, a,# ) = (si,##) / * memorisation de 11 a 11 par le symbole# * /
1 (s1, b, # ) = (s2, e) / * début de vidage de la pile * /
1 (s2, b, # ) = (s2, e) / * vidage de la pile en dépilant tous les symboles # */
• L'analyse du mot 11 aaabbb 11 nécessite les pas suivants :
(sa, aaabbb, #) l-
(s1, aabbb, #) i-
(si, abbb, # #) l-
(s1, bbb, # # #) i-
(s2, bb, # #) l-
(s2, b, #) i-
(s2, e ,e)
C'est une configuration de « Succès », et le mot est accepté par critère de pile
vide.
• Analyse du mot 11 aab 11 n'appartenant pas à L, à priori. On va le confirmer
à l'aide des pas suivants :
(sa, aab, #) 1-
(si, ab, #) l-
(s1, b, # #) l-
(s2, e, #) Blocage
90 Chapitre 3

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.

(:\ a/# (a#) ~


~~

Figure 44 : Diagramme de l'automate à pile (mode état final) de


L = {anbn 1 n ~ 1}
Conformément au diagramme de transition de la Figure 44, l'automate à pile
en mode état final est défini par Af = (S, sa, VT, r, #, F, I) où :
S = {sa, si, s2, f} est l'ensemble fini des états de l'automate.
L'état sa est l'état initial de l'automate.
VT ={a, b} est l'alphabet terminal.
Le symbole # représente le symbole initial (fond de la pile).
r= {a, #}est l'alphabet auxiliaire de la pile.
F = {f}.
La fonction de transition est définie conformément au diagramme par :
1 (sa, a, #) = (s1, a# )
1 (si, a, a) = (s1, aa)
1 (si, b, a) = (s2, e)
1 (s2, b, a) = (s2, e)
1 (s2, e, #) = (f, #)
• L'analyse du mot "aaabbb" nécessite les pas suivants :
(sa, aaabbb , #) l-
(s1, aabbb, a#) 1-
(si, abbb, aa #) 1-
(si, bbb, aaa #) 1-
(s2, bb, aa #) l-
(s2, b, a#) 1-
(s2, e, #) 1-
(f, €, #)
L'automate serait à critère d'acceptation généralisé (pile vide et état final) si la
dernière transition, à savoir, I(s 2, e, # ) = (f, #) était plutôt I(s 2, e, # ) = (f, e).
Grammaires hors contexte et automates à pile 91

Ainsi, le dernier pas de l'analyse serait (s2, e, #) 1- (f, e, e) au lieu de (s2, e, #)


1- (f, e, #).
• L'analyse du mot "abb" nécessite les pas suivants :
(so, abb, #) l-
(si, bb, a#) 1-
(s2, b, #)
La dernière configuration (s2, b, #) ne mène nulle part, c'est une configuration
de blocage, car il n'existe aucune transition à partir de l'état s2 qui mène vers un
certain état avec le symbole "b" sachant que le sommet de pile est égale à #.
Autrement dit, 1 (s2, b, #) = 0.
Remarque 2.3
Un langage est accepté par un automate à pile avec le mode de reconnaissance sur
pile vide si et seulement s'il est accepté par un automate à pile avec le mode état
final.
Pour passer du mode pile vide au mode état final, il suffit de faire en sorte que
chaque transition, où le symbole du fond de pile est dépilé, est remplacée par
une transition vers un nouvel état final.
Réciproquement, le passage du mode état final au mode pile vide nécessite,
après avoir atteint un état final, de vider entièrement la pile.
L'automate en mode pile vide du langage L = {an bn 1 n ~ 1} de la Figure 43,
ne peut pas être transformé directement, tel qu'il est, en automate à pile en mode
état final selon la règle précédente. Mais, afin de créer les conditions d'application
de la règle de passage, on introduit un nouveau symbole de pile noté Z qui restera
au fond de la pile jusqu'à la fin (à la reconnaissance d'un mot), pour être retiré.
Lors du retrait de Z de la pile, l'automate transite vers un nouvel état final noté
v. L'automate sous sa nouvelle forme est illustré par le diagramme de la
Figure 45.

b/# (e)

Figure 45 : Automate à pile (mode pile vide) de L = {an bn 1 n ~ 1}

Enfin, par application de la règle de passage sur l'automate de la Figure 45,


on obtient l'automate représenté par le diagramme de transition de la Figure 46.
Le seul changement concerne la dernière transition, à savoir, 1 (s2, e, Z) = (v, e)
sur la Figure 45, qui est remplacée par I(s 2, e, Z) = (f, Z), comme illustré par le
diagramme de la Figure 46. L'état « v » qui était un état où se vide la pile a été
effectivement remplacé par un nouvel état final « f », où la pile, n'est pas
forcément vide.
92 Chapitre 3

On aurait pu cependant avoir également le mode d'acceptation généralisé


[pile vide et état final simultanément] ; il suffit de remplacer la transition
1 {s2 , e, Z) = {f, Z) par la transition I(s 2 , e, Z) = {f, e).

a/Z (#Z) b/# (e)

~G )~ )
<Y*:\_)
~
~

Figure 46 : Automate à pile (mode état final) de L = {an bn 1 n;::: 1}

On peut remarquer que le diagramme de la Figure 46 est la copie conforme du


diagramme de la Figure 44, à quelques symboles prés. En effet, en remplaçant
dans la pile respectivement les symboles {Z par # et # par "a"), au niveau du
diagramme de la Figure 46, on retrouve exactement le diagramme de la
Figure 44.
L'application de la règle inverse, à savoir, le passage de l'automate en mode
état final vers l'automate en mode pile vide, nécessite d'introduire un nouvel état
où se vide la pile à la fin (à la reconnaissance d'un mot du langage L = {an bn 1 n
;::: 1}). Ainsi, en considérant l'automate de la Figure 46, on introduit un nouvel
état « t » pour le vidage de la pile tel que 1 {f, e, Z) = (t, e). Mais pour simplifier,
il suffit de considérer l'état final « f » comme un état de vidage de la pile, c'est-à-
dire 1 (s 2 , e, Z) = {f, e) au lieu de 1 {s2 , e, Z) = {f, Z). On retrouve ainsi un
automate en mode d'acceptation par pile vide, similaire à celui de la Figure 44.
On peut même garder « f » comme un état final et de pile vide à la fois, si l'on
souhaite obtenir un mode d'acceptation généralisé.
Construire un automate à pile qui accepte L = {anbn 1 n;::: 1}, en utilisant une
grammaire à contexte libre G telle que L{G) = L.
Il faut noter que l'utilisation d'une grammaire à contexte libre, pour construire un
automate à pile, est le résultat d'une démonstration ayant pour but de montrer
que le langage accepté par un automate à pile est un langage à contexte libre.
Ainsi, en considérant une grammaire à contexte libre G = (VN, VT, Z, P), on peut
construire un automate à pile vide AE = ( {s0}, s0 , VT, VN uVT, Z, 0, I) où la
fonction de transition 1 est définie par :
• Si A~ a. E P, alors (so, a.) E 1 {s0 , e, A).
• 1 (so, a, a) = {(so, e)} V a E VT.
Cet automate possède un unique état noté s0 duquel partent et auquel arrivent
toutes les transitions. La nécessité d'ajouter un ou plusieurs nouveaux états
provient du besoin d'obtenir un automate déterministe.
Quand une grammaire admet des règles de la forme A ~ aa. avec a E VT et
a. E V *, le terminal "a" peut être consommé dans une unique transition notée
1 (s, a, A) = {s, a.) qui englobe les deux transitions I{s, e, A) = {s, aa.) et
1 (s, a, a) = (s, e).
Grammaires hors contexte et automates à pile 93

Une grammaire qui engendre le langage L = {an bn 1 n ~ 1} est définie par le


quadruplet G = (VN = {Z}, VT = {a, b}, Z, P) avec l'ensemble des productions
P = {Z ~ aZb 1 ab}.
L'automate à pile vide équivalent construit à base de cette grammaire est
défini par le 7-uplet Ae = ({so}, so, {a, b}, {Z, a, b}, Z, 0, 1) avec la fonction de
transition suivante :
1 (s 0 , e, Z) = {(s0 , aZb), (s0 , ab)}; multi définition de la fonction de transition
indiquant que l'automate n'est pas déterministe. On définira sous peu les
conditions pour qu'un automate à pile soit déterministe.
1 (so, a, a) = (so, e)
1 (so, b, b) = (so, e)
• L'analyse du mot 11 aaabbb 11 génère les pas suivants :
(so, aaabbb, Z) 1-
(so, aaabbb, aZb) 1-
(so, aabbb, Zb) 1-
(so, aabbb, aZbb) 1-
(so, abbb, Zbb) 1-
(so, abbb, abbb) 1-
(so, bbb, bbb) 1-
(so, bb, bb) l-
(so, b, b) l-
(so, e, e)
C'est une configuration de « Succès ». Le mot a été accepté, mais avec un nombre
de pas plus élevé qu'avec l'automate construit intuitivement pour le même
langage. Ceci est dû, principalement à l'e-transition I(so, e, Z) = {(s0 , aZb),
(so, ab)}. On peut remplacer l'e-transition par la transition 1 (s 0 , a, Z) =
{(so, Zb), (so, b)}. On obtiendra donc pour le même mot 11 aaabbb 11 moins de pas
comme suit:
(so, aaabbb, Z) 1-
(so, aabbb, Zb) 1-
(so, abbb, Zbb) 1-
(so, bbb, bbb) 1-
(so, bb, bb) 1-
(so, b, b) 1-
(so, e, e)
C'est une configuration de « Succès » obtenue en un nombre de pas d'analyse égal
à celui obtenu auparavant pour le même mot avec l'automate à pile vide construit
intuitivement pour le même langage.
L'analyse aurait pu engendrer plus de pas à cause de la transition 1 (so, a, Z)
qui admet deux valeurs {(s0 , Zb), (s0 , b)}. Autrement dit, si on avait choisi, à
chaque fois, la valeur (s 0 , b) en premier, plutôt que (so, Zb), on aurait eu plus de
pas, puisque il aurait fallu tester toutes les alternatives ( {(s0 , Zb ), (s 0 , b)}) d'une
transition pour décider quelle est celle qui convient. Le problème de choix d'une
94 Chapitre 3

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.

Définition 2.2 (Déterminisme de l'automate à pile)


La fonction de transition est définie par 1 : S X (VTU{ E}) X r ~ S X r *.
On note le cardinal (nombre d'états atteints à partir d'un état donné) de la
fonction de transition par le terme II(s, a, A)I où s E s, a E VTU{E} et A E r.
Un automate à pile est déterministe si les deux conditions suivantes sont
satisfaites :
- V (s E S, a E VTU{E} et A E r) II(s, a, A)I :s; 1. Autrement dit, il y a au
plus une seule transition à partir d'un état « s » pour un même symbole "a"
e VTu{E} et un même sommet de pile A, V (se S, a E VTU{E}, A E r).
- V (s E S, A e r) si I(s, E, A) -:/:. 0 , alors I(s, a, A) = 0, V a E VT.
Les automates à pile précédents acceptant le langage L = {an bn 1 n ~ 1}, sont
tous déterministes, excepté celui construit directement sur la base des règles
Z ~ aZb 1 ab, de la grammaire G telle que L(G) = L.

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.

3 Automate à pile étendu

Définition 3.1 (Automate à pile étendu)


Un automate à pile étendu est un modèle d'analyseur ascendant défini
formellement par le 7-uplet Ad= (S, So, VT, r, #, F, Id) où:
S est l'ensemble des états
so est l'état initial
VT est l'alphabet terminal de base de l'automate
r est l'alphabet de pile
# est l'élément de fond de pile e r dit aussi symbole initial de pile
Id est la fonction de transition définie par Sx(VTu{E})xr•~p(Sxr •)
F ~ S est l'ensemble fini des états finals

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

Soit une lecture (nommée aussi décalage).


Soit une réduction (inverse de la dérivation).
Soit la détection de la fin de l'analyse.
Soit un blocage qui est un cas d'erreur.
Ainsi, soit G = (VN, VT, Z, P) une grammaire hors contexte ; la fonction de
transition Id de l'automate Ad= ({sa, qr}, sa, VT, (VNUVT)u{#}, #, {qr}, Id) est
définie par :
Id (sa, a, e) = {(sa, a)} V a E VT, qui indique qu'il faut lire le symbole 11 a 11 et
l'empiler. Ce genre de transition est surnommé décalage pour indiquer qu'il y
a à la fois lecture d'un symbole (déplacement de la tête de lecture), ainsi que
le stockage du symbole lu (empilement) dans la pile.
Si A ~ a. E P, alors (sa, A) E I (sa, e, a.), qui signifie qu'il faut dépiler la
chaine a. E v* = (V NUVT )*, et empiler le symbole non-terminal A E VN à la
place. On surnomme réduction (a. ~ A) ce type de transition, conformément
aux règles de production A ~ a. E P.
I(sa, e, #Z) = {(qr, e)} qui indique la fin de l'analyse et transite vers l'état
final qr.
Tout autre cas est une erreur (blocage)
Tout comme l'automate à pile, modèle d'analyseur gauche (ou descendant)
défini plus haut, l'automate à pile étendu (modèle d'analyseur ascendant) est
également représentable graphiquement.
Les transitions de l'automate à pile étendu vont être déterminées par trois
éléments correspondant à :
l'état courant ;
le symbole courant sous la tête de lecture ;
la chaine apparaissant au sommet de pile.
Il y a trois modes d'acceptation (pile vide, état final et généralisé) d'un langage
hors contexte par un automate à pile étendu, mais c'est le mode généralisé qui est
le plus souvent utilisé, c'est-à-dire :
L (Adg) = {ro E vT* 1 (sa, ro, #) 1-* (q, e, #Z) 1-- (sf, e, e) }, avec Sf E
F, et la pile est vide à la fin de l'analyse.

3.1 Fonctionnement de l'automate à pile étendu


Pour définir de manière précise le comportement d'un tel automate, on introduit
la notion de configuration. Cette dernière est une représentation concise de
l'automate définie par le triplet (s, \j/, y), où :
« s » est l'état courant de l'automate ;
\jlest la sous-chaine courante (non encore lue) ;
y est le contenu de la pile.
Les différentes configurations possibles d'un tel automate sont :
Configuration générale (s, \j/, y) où « s » est un état quelconque E S, \jl une
sous chaîne de VT * en cours d'analyse (non encore lue) ; et y E ['* est le
contenu de la pile .
www.bibliomath.com
Grammaires hors contexte et automates à pile 97

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.

3.2 Construction de l'automate à pile étendu


On construit ce type d'automate en s'appuyant, en partie, sur les règles de
production d'une grammaire à contexte libre.
Pour avoir une idée plus claire sur cette construction on se propose de
construire un automate qui accepte le langage L = {an bn J n ~ 1}.
On sait qu'une grammaire G = (VN, VT, Z, P) qui génère L, a pour
productions Z ~ aZb 1 ab. Pour une représentation concise de l'automate, on peut
adopter la notation graphique.
Tout comme l'automate à pile gauche, on suppose que le symbole initial de
pile # est déjà au fond de la pile.
Le diagramme de transition de la Figure 4 7 est une représentation graphique
de l'automate étendu Ad= ({sa, qr}, sa, {a, b}, {Z, #, a, b}, #, {qr }, Id) avec la
fonction de transition Id définie par :

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

(so, aabbb, #a) 1- Décaler "a"


(so, abbb, #aa) 1- Décaler "a"
(so, bbb, #aaa) 1- Décaler "b"
(so, bb, #aaab) 1- Réduire "ab" en Z
(so, bb, #aaZ) 1- Décaler "b"
(so, b, #aaZb) 1- Réduire "aZb" en Z
(so, b, #aZ) 1- Décaler "b"
(so, E, #aZb) 1- Réduire "aZb" en Z
(so, E, #Z) 1- Acceptation
(Clf, E, E) 1-Stop
La configuration indique que le mot "aaabbb" a été totalement lu, la pile est vide
et l'automate a atteint son état final.
• Le mot "aab" génère la séquence d'analyse suivante :
(s 0 , aab , #) 1- Décaler "a"
(s 0 , ab, #a) 1- Décaler "a"
(s 0 , b, #aa) 1- Décaler "b"
(s0, E, #aab) 1- Réduire "ab" en Z
(so, E, #aZ) 1- Blocage
Le mot a été entièrement lu, mais la pile n'est pas vide et l'état s0 , n'est pas final.
C'est une configuration de blocage (qui ne permet ni décalage ni réduction) qui ne
mène nulle part. Autrement dit, le mot "aab" n'est pas accepté par l'automate Ad.

Définition 3.2 (Déterminisme de l'automate à pile étendu)


La fonction de transition est définie par Id: S x (VTU{E}) x ï * ~ S x ï *.
Un automate à pile étendu est déterministe si on vérifie les deux conditions
suivantes :
"<;'/ (s E S, a E VTu{e}, a E ï) * lld (s, a, a)I :::; 1. Autrement dit, il y a au plus
un seul arc sortant d'un état« s » pour un même symbole "a" E VTU{E} et
une même chaine a au sommet de pile, "<;'/ (s E S, a E VTu{E}, a E ï *).
*
Si Id (s, a, a) '# 0 et Id (s, b, ~) 0 avec a = b ou b = E, les chaînes a et~
(avec a '# ~) doivent être telles qu'aucune d'elles ne doit être le suffixe de
l'autre. Autrement dit, il n'existe pas de chaînes y et ô telles que a=~ ou
~ = ôa, avec a, ~. y et ô E ï *.

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

lct (so, E, #S)


ne respecte pas les conditions du théorème précédent. En effet, en considérant, par
exemple, les deux transitions (so, E, T) = (so, S) et lct (so, E, S + T) = (so, S), à
l'état s0, avec le symbole E, les deux contenus de pile T et S + T ont le même
suffixe. Il suffit de remarquer que S + T s'écrit en fonction de T. Le théorème dit
que si a.= Tet ~ = S + T alors~ = ôa. = ôT, ce qui suppose ô = s+.

De même, l'automate de la Figure 47, n'est pas déterministe, car la deuxième


condition du théorème n'est pas vérifiée non plus. En effet, les transitions lct (s 0 , a,
e) et lct (so, b, e) rentrent en conflit avec les deux autres transitions lct (so, E, ab)
et lct (so, E, aZb). Par exemple, avec les transitions lct (so, a, e) -:!- 0 et lct (so, E, ab)
-:!- 0 on a a. = E et ~ = ab, c'est-à-dire qu'il existe ô = ab tel que ~ = ab = ôa..

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.

Ainsi, pour construire un automate à pile étendu déterministe qui reconnait le


langage L, on reconsidère l'automate de la Figure 47 et faire en sorte que les
conditions du théorème précédent soient satisfaites.

L'automate déterministe Act = (S, s0 , VT, (VNUVT)u{#}, #, {f}, lct) est


représenté graphiquement par le diagramme de transition de la Figure 48, avec
S = {so, p, r, t, s, f}.

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----

Figure 48 : Automate à pile étendu déterministe pour L = {a0 b 0 1 n;;:: 1}

• Analyse du mot "aab" :


(so, aab$, #) 1- Décaler "a"
{p, ab$, #a) 1- Décaler "a"
{p, b$, #aa) 1- Décaler "b"
(r, $, #aab] 1- Réduire "ab" en Z
(t, $, #aZ) Blocage
Il n'existe aucune transition qui permet d'avancer, c'est une situation de blocage.
Donc, le mot "aab" est rejeté par l'automate.

• Analyse du mot "abb" :


(so, abb$, #) 1- Décaler "a"
{p, bb$, #a) 1- Décaler "b"
(r, b$, #ab) 1- Réduire "ab" en Z
(t, b$, #Z) 1- Décaler "b"
(s, $, #Zb) Blocage
C'est une situation de blocage, car il n'y a pas de transition qui permet de
continuer. Autrement dit, le mot "abb" n'est pas accepté par l'automate.

4 Automate à pile fondé sur un réseau d'automates finis


Ce modèle d'automate à pile est issu d'un RAF {Réseau d'Automates Finis). Ce
dernier est une représentation graphique d'une grammaire à contexte libre.
A titre indicatif, la Figure 49, donne une idée sur les diagrammes du réseau
d'automates finis de la grammaire Z ~ AB; A ~ aA 1 a; B ~ bB 1 b. Il y a
autant de diagrammes de transition dans le réseau (RAF) qu'il y a de symboles
non-terminaux distincts de l'ensemble VN.
Pour simplifier, on reconsidère la grammaire à contexte libre dont les règles sont
Z ~ aZb 1 ab, et on fait, en sorte, comme si elle était une grammaire régulière.

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:

Figure 49: Exemple de réseau d'automates finis

Autrement dit, elle peut être représentée par un diagramme de transition


comme celui de la Figure 50.

Z:

Figure 50: RAF de la grammaire Z ~ aZb 1 ab

Un RAF admet deux types de transition :


Les transitions ordinaires: comme celles qu'on a l'habitude de rencontrer
sur un automate d'états finis (sans pile), comme par exemple I(s, a) = {t, q},
I (r, b) = f et I (q, b) = f, qui sont considérées comme étant des transitions
ordinaires.

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 est à l'état « t », il y a une sauvegarde (empilement) de


l'état « r » et une transition vers l'état initial (nommé Z) du diagramme de
l'automate correspondant aux règles Z ~ aZb 1 ab. Cette transition est
considérée comme un appel à une procédure nommée Z.

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.

Définition 4.1 (Automate à pile construit sur la base d'un RAF)


Soit R = (Ai, A2 1 ••• 1 An) un RAF tel que Ai = (ViuS, Si, Fi, soit li) pour
1 s; i s; n.
On définit un automate à pile A = (S, s0 , VT, r, #, F, I) tel que L(A) = L (R)
où:
S = UP=i Si l'ensemble des états de A.
s0 = s01 (l'état initial de A est aussi l'état initial de A1).
VT = ur=l vi l'ensemble des symboles terminaux.
r = {#}u{UP=1 Ii(s, "q")} l'alphabet de la pile de A, où s, q E S.
Fi:;;; S = UP=i Fi l'ensemble des états finals de A.
La fonction de transition 1 de A est telle que :
• Si li (s, a) = q, alors 1 (s, a, X) = (s, X), où s et q E S, a E VT, et X E r.
Ce type de transition n'a aucune action sur la pile.
• Si li (s, p) = q, alors 1 (s, E, X) = (p, qX), où s, p, q E s et X E r
correspond à un appel, et signifie empiler l'état « q » pour pouvoir
réaliser le retour ultérieurement.
• ("1 r E r et q E F) (1 (q, E, r) = (r, e)), représente le «retour» réalisé
par l'action « dépiler».
Pour illustrer cette définition, on se propose de construire un automate à pile
pour la grammaire dont les règles sont :
Z ~ ZC 1 a
C ~ BC 1 b
B~Ca

Le RAF préliminaire correspondant est décrit par les diagrammes de transition


de la Figure 51. Mais, avant de construire effectivement l'automate à pile associé,
il va falloir d'abord annoter convenablement les diagrammes de la Figure 51, à
savoir :
Nommer tous les états par les symboles de S (S est le futur ensemble des états
de l'automate à pile A envisagé).
Remplacer les symboles non-terminaux Z, C, B par les symboles
correspondants dans l'ensemble des états S.
www.bibliomath.com
Grammaires hors contexte et automates à pile 103

Une fois l'opération d'étiquetage terminée, on obtient les diagrammes de


transition convenablement annotés de la Figure 52. Ces derniers constituent le
RAF permettant de construire enfin directement l'automate à pile envisagé, selon
l'approche définie ci-dessus.

Règles
Z ~ ZC 1 a Z:

Règles
C ~ BC 1 b C:

Règle B ~Ca

Figure 51 : RAF préliminaire des règles Z ~ ZC 1a;C~BC1 b; B ~Ca

Règles
Z ~ ZC 1 a s1 :

Règles
C ~ BC 1 b s4:

Règle B ~Ca

Figure 52 : RAF finalisé des règles Z ~ ZC 1 a ; C ~ BC 1 b ; B ~ Ca


D'après les diagrammes de la Figure 52, on a les fonctions de transition li, h
et l3:
11 (s1, s1) = s2
11 (si, a) = s3
11 (s2, s4) = s3
12 (s4, s7) = s5
12 (s4, b) = S5
www.bibliomath.com
104 Chapitre 3

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)

- Analyse du mot "abab"


(s1 1 abab, #) l-
(s1, abab, s2#) i-
(s3 1 bah, s2#) l-
(s2, bah, #) i-
(s4 1 bah, s3#) l-
(s7 1 bah, s5s3#) i-
(s4, bah, SgS5S3#) l-
(s6, ab, SgS5S3#) i-
(ss, ab, S5S3#) l-
(s 9 , b, s5s3#) i-
(s5, b, s3#) l-
(s4 1 b, s5s3#) i-
(S5, E:, S5S3#) l-
(s5 1 E:, s3# i-
(s3, e, #) Succès

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:

Figure 53: RAF préliminaire associé à la grammaire des expressions


arithmétiques

M:

E
l
M:
M: --+

Figure 54: Simplification des diagrammes associés aux règles M --7 +TM 1 e
et E --7 TM

Avant la construction effective de l'automate envisagé, on peut simplifier les


diagrammes de la Figure 53. Un aperçu de cette simplification est présenté par la
Figure 54:
Les diagrammes finalisés annotés et simplifiés dérivés du RAF préliminaire de
la Figure 53 sont récapitulés dans la Figure 55.
www.bibliomath.com
Grammaires hors contexte et automates à pile 107

Figure 55 : RAF simplifié des expressions arithmétiques

L'automate à pile associé résultant est défini par A = (S, s0 , VT, r, #, F, I)


où:
S = { s1, s2, s3 1 s4 1 s5, s6, s7, ss}
L'état s0 = s1 est l'état initial de l'automate à pile A
VT ={a,+, *, (, )}
r = {#}u{s2, S4, s7}
# est le symbole initial de pile (fond de pile)
F = {s2 1 s4 1 sa}
La liste des valeurs de la fonction de transition est :
1 (s1 1 E, X) (s3, s2X), X E r
1 (s2 1 +,X) = (si, X)
1 (s3 1 E, X) = (ss, s4X)
1 (s4 1 *,X) = (s3 1 X)
1 (ss, (, X) = (s6, X)
I (s6, E, X) = (si, s1X)
1 (s1 1 ) , X) = (ss, X)

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, #)

L'expression "a * (a + a)" a été acceptée par l'automate A. On aurait pu


arrêter l'analyse à l'état final ss, puisque l'analyse de l'expression est terminée au
niveau de cet état et que, de plus, on n'avait pas besoin de vider la pile. Le mode
d'acceptation de ce type d'automate est par état final.
Remarque 4.2
Tout comme l'automate issu du RAF associé à la grammaire Z ~ ZC 1 a;
C ~ BC 1 b ; B ~ Ca, l'automate présenté ci-dessus, lui non plus, n'est pas
déterministe.
En effet, par exemple, la coexistence des transitions 1 (s2, +, s1) = (s1, s1) et 1 (s2,
e, s1) = (s1, e) est un non-respect des conditions pour qu'un automate à pile soit
déterministe.

On peut toutefois concevoir un automate à pile gauche déterministe


intuitivement directement, sans passer par une grammaire ni un RAF. L'existence
de cet automate est garantie, car le langage des expressions arithmétiques est
analysable de maniêre descendante déterministe.

L'automate à pile déterministe représenté par le diagramme de transition de la


Figure 56 a été conçu intuitivement en s'appuyant sur l'agencement des
opérandes, des parenthêses et des opérateurs, les uns avec les autres, etc.
www.bibliomath.com
Grammaires hors contexte et automates à pile 109

a/# (#)

+,*/# (#)
( / #(##)

Figure 56 : Automate à pile déterministe qui reconnait les expressions


arithmétiques

L'analyse de l'expression "(a + a) * a" par l'automate de la Figure 56


engendre les pas suivants :
(sa, (a+ a) *a, #) 1-
(sa, a+ a)* a, ##) 1-
(si, +a) *a, ##) 1-
(sa, a) *a, ##) 1-
(s1, ) *a, ##) 1-
(s1, *a, #) 1-
(sa, a, #) 1-
(si, e, #) Stop
La chaine à analyser est terminée, et l'automate a atteint son état final, en un
nombre de pas très réduit (7 pas) comparativement à l'automate à pile issu du
RAF de la Figure 55 (23 pas, si l'automate s'arrête à l'état final s8 ).

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.

Définition 5.1 (Transducteur à pile)


Formellement, un transducteur à pile est défini par l'octet
Tp= (S, sa, VTi, VT2, r, #, F, I) où:
S est l'ensemble fini non vide des états.
L'état sa e S, est l'état initial.
VT1 et VT2 sont respectivement les alphabets d'entrée et de sortie.
r l'alphabet auxiliaire de la pile.
# E r est le symbole initial (du fond de la pile).
F ç S l'ensemble des états finals.
La fonction de transduction 1: Sx(VT1U{e})xr~p(S X r * X V *T2)
www.bibliomath.com
110 Chapitre 3

Quand le transducteur est déterministe la fonction de transduction s'écrit


tout simplement comme 1: Sx(VT1U{E})xr~s x [' * x V *T2

---,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

Figure 57: Machine abstraite représentant un transducteur à pile

5.1 Fonctionnement du transducteur à pile


Pour définir de manière précise le comportement d'un transducteur, il convient
d'introduire la notion de configuration. Une configuration est une représentation
concise du transducteur dans une situation donnée. Elle est définie par le
quadruplet (s, coi, y, ffi2) où :
« s » est l'état courant de l'unité de contrôle,
co1 est la sous-chaine non encore lue (courante),
co1 est la sous-chaine déjà écrite (courante),
y est le contenu de la pile.

Les différentes configurations possibles d'un transducteur à pile sont :

La configuration générale (s, coi, y, ffi2), où « s » est un état quelconque e S,


co1 est une sous-chaîne courante (eV*Ti), présentée à l'entrée du transducteur,
y est le contenu courant de la pile (e r*), et enfin, ffi2 est la sous-chaine
courante obtenue en sortie.

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

Transition d'une configuration donnée à une configuration successeur


V (s e S, a e VT 1u{e}, A e r) si (p, a., v) e 1 (s, a, A) alors (s, am, Ao ,\jf) 1-
(p, m, a.o, \jfv) avec veV*T2, p e S, a., o e r,
* me V*Tl et 'I' e V*T2·

Modes d'acceptation (ou de transduction) :


On dit que w e V*T2 est la traduction de U E y*Tl si (so, U 1 #, E) 1-
(f, E, a., w) pour un certain f e F et a. E r *. Le mode d'acceptation
(transduction) ici, est par état final.

De même, on peut dire que w E v* T2 est la traduction de u E v* Tl par pile


vide, si on a (s0 , u, #, e) 1- (p, e, e, w) pour un certain p e S. Le mode
d'acceptation est par pile vide.

On peut également avoir les deux modes, par état final et par pile vide
simultanément.

5.2 Construction d'un transducteur à pile


On aurait pu envisager la construction de ce type de transducteur en s'inspirant
de la construction de l'automate â pile de la même catégorie. Cependant,
contrairement â ce dernier, cette construction est beaucoup plus souvent basée sur
une grammaire â contexte libre. En effet, les règles d'écriture de la grammaire se
prêtent particulièrement mieux â la traduction. Les différents exemples suivants
donnent une idée précise sur cette construction.
Par exemple, soit â construire un transducteur â pile qui reconnaît une
expression arithmétique simple préfixée, et la traduit en sa correspondante
post-fixée.
Un transducteur construit intuitivement, est Tp = (S, So, VT1, VT2, r, #, F, I)
où:
S = {s}.
L'état s0 = s, est l'état initial du transducteur.
VT1 ={a,+,*}.
VT2 ={a,+,*}.
r= {+, *, #}.
# est le symbole initial (ou fond) de la pile
F = {s}
La fonction de transduction est définie comme suit :
1 (s, a, #) = (s, e, a)
1 (s, +, #) = (s, ##+, e)
1 (s, *, #) = (s, ##*, e)
I(s,e,+) = (s,e,+)
I(s,e,*) = (s,e,*)
L'analyse de l'expression préfixée 11 + a * a a 11 génère les pas suivants :
(s, +a*aa, #, e) 1-
(s, a*aa, ##+, e) 1-
www.bibliomath.com
112 Chapitre 3

(s, *aa, #+, a) 1-


(s, aa, ##*+, a) 1-
(s, a, #*+, aa) 1-
(s, E, *+, aaa) 1-
(s, E, +, aaa*) 1-
(s, E, E, aaa*+)
L'expression préfixée "+ a * a a" a été acceptée et traduite en la forme post-fixée
"a a a * +" correspondante.
Un autre exemple consiste à construire un transducteur à pile qui reconnait
une expression arithmétique correctement parenthésée, et la traduit en sa
correspondante post-fixée.
On peut dans un premier temps construire un STDS (Schéma de Traduction
Dirigée par la Syntaxe) qui permet de mettre en correspondance n'importe quelle
expression arithmétique infixée avec l'expression post-fixée cible associée.
Le schéma de traduction est décrit par la liste des règles numérotées
suivantes:
S~A+Z (1) AZ+
S~A (2) A
A~B*A (3) BA*
A~B (4) B
B ~ (Z) (5) z
B~a (6) a
Le membre droit de chaque règle de production est décoré par son numéro,
ainsi que la représentation post-fixée correspondante. Ceci garantit une traduction
dirigée par la syntaxe. Cependant, au lieu de concevoir directement le
transducteur, on se propose de diviser le travail en deux phases :
Dans la première phase, on s'intéresse au transducteur d'un point de vue
syntaxique, en utilisant :
• soit la grammaire (partie syntaxique du STDS),
• soit un transducteur préliminaire spécial qui reçoit en entrée une expression
arithmétique et produit en sortie les numéros des règles de production
utilisées.
La deuxième phase consiste en la conception d'un deuxième transducteur qui
reçoit en entrée les numéros des règles de production obtenues par la
grammaire (ou le transducteur), au cours de la phase précédente, et produit en
sortie l'expression post-fixée correspondante conformément au STDS.
On choisira plutôt la grammaire, pour la première phase, c'est moins
contraignant et plus facile à comprendre. De ce fait, on se focalise uniquement sur
la phase de traduction.
On définit alors le transducteur Tp = ({p}, p, {1, 2, 3, 4, 5, 6}, {a, +, *},
{Z, A,+,*, a}, Z, 0, I) par la fonction 1 de transduction suivante:
1 (p, 1, Z) = (p, AZ+, E)
1 (p, 2, Z) = (p, A, E)
www.bibliomath.com
Grammaires hors contexte et automates à pile 113

I (p, 3, A)= (p, BA*, e)


I (p, 4, A)= (p, B, E)
I {p, 5, B) = {p, Z, E)
I {p, 6, B) = {p, a, E )
I {p, e, a)= {p, E, a)
I {p, e, *) = {p, e, *)
I {p, E, +) = {p, E, +)

- Analyse et traduction de l'expression "a* (a+ a)"


{2) Z =>A
(3) A=> B *A
(6) B * A => a * A
{4) a* A =>a* B
(5) a * B => a * {Z)
{1) a * {Z) => a * (A + Z)
(4) a * {A + Z) => a * {B + Z)
{6) a* {B + Z) =>a* (a+ Z)
{2) a *(a+ Z) =>a* (a+ A)
{4) a *(a+ A) =>a* (a+ B)
{6) a *(a+ B) =>a* (a+ a)

L'analyse par la grammaire a donc produit la dérivation canonique 2 3 6 4 5 1 4 6


2 4 6. Cette dernière sera présentée à l'entrée du transducteur défini ci-dessus. La
traduction finale a donc utilisé les pas suivants :

{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

Définition 5.2 (Déterminisme du transducteur à pile)


Tout comme l'automate à pile gauche, un transducteur à pile est déterministe
si les deux conditions suivantes sont satisfaites :
V (s E s, a E VT1U{ë}, A E r ) JI(s, a, A)J ~ 1
V (s E S, A E r) si I(s, e, A) -:;:. 0 alors I(s, a, A) = 0, V a E VT1·

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

En combinant les deux sous-ensembles de règles ci-dessus, on aura la liste


finalisée suivante :
S --7 E +E J -E
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.

Ecriture de la grammaire ci-dessus sous forme EBNF :


S~{+J-}E
E~T{+TJ-T[ *
T ~ F {*F l/F}
F ~ K {~K}
K ~ i 1 n 1 (S) 1 R
R ~ i (L)
L ~ S {, S} *

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

On peut s'inspirer des grammaires des expressions arithmétiques ou logiques.


Outre les parenthèses, le langage des expressions régulières comporte deux
opérateurs binaires respectivement l'union « Etl » et la concaténation « • », ainsi
que deux opérateurs unaires sous forme d'exposants, qui dénotent respectivement
l'itération réflexive-positive « * » et l'itération positive « + ».

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.

Afin de ne pas trop charger le diagramme, et pour simplifier l'écriture, les


symboles u, p et '6, ont été utilisés sur le diagramme de l'automate pour désigner
respectivement les ensembles {a, E, 0}, {+,*}et{•,®}.
La chaine 11 (a* ® e)+•0 11 , doit être accEptée par un tel automate, comme c'est
illustré par la séquence d'analyse suivante :
118 Chapitre 3

(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

u /# (#) p/# (#)

) /#(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 /-:

·~ (ii) : RAF finalisé

q ::/:

On peut facilement déduire la fonction de transition à partir du RAF finalisé


(ii), ce qui donne alors ce qui suit :
I (s, a, Z) = {(q, Z), (t, Z)}
I (q, b, Z) = (f, Z)
I (t, E, Z) = (s, rZ)
I (r, b, Z) = (f, Z)
I (f, E, s) = (s, E)
Cet automate n'est pas déterministe, à cause de la transition I (s, a, Z) =
{(q, Z), (t, Z)}. On peut factoriser la grammaire et obtenir ainsi les règles Z ~ aA
et A~ b 1 aAb.
Le nouveau RAF associé à la grammaire est décrit par les diagrammes
suivants:

A:
Grammaires hors contexte et automates à pile 121

On déduit finalement l'automate à pile déterministe dont la fonction de


transition est :
1 (s, a, X) = (p, X)
1 (p, E, X) = (r, fX)
1 (r, b, X) = (q, X)
1 (r, a, X) = (t, X)
1 (t, E, X) = (r, uX)
1 (u, b, X) = (q, X)
1 (f, E, f) = (f, E)
1 (f, E, u) = (u, e)
1 ( q, E, f) = (f, E)
1 ( q, E, U) = ( U, E)
On a donc obtenu l'automate déterministe Ap = (S, sa, VT, r, #, F, I) où
S = {s, p, f, r, t, u, q}
Sa= S
VT ={a, b}
r = {#}u{f, u}
F = {f, q}

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

(s, +a, NM, 14858) l-


(s, +a, M, 148586) i-
(s, a, TM, 1485862) l-
(s, a, FNM, 14858624) i-
(s, e, NM, 148586248) l-
(s, e, M, 1485862486) i-
(s, e, e, 14858624863) 1-
L'expression est acceptée, et ce premier transducteur a généré la dérivation
canonique gauche 7t1 = 14858624863 qui sera analysée par le deuxième
transducteur. Ce qui donne la séquence d'analyse suivante :
(p, 14858624863, E, e) 1-
(p, 4858624863, TM, e) 1-
(p, 858624863, FNM, e) 1-
(p, 58624863, aNM, e) 1-
(p, 58624863, NM, a) 1-
(p, 8624863, FN*M, a) 1-
(p, 624863, aN*M, a) 1-
(p, 624863, N*M, aa) 1-
(p, 24863, *M, aa) 1-
(p, 24863, M, aa*) 1-
(p, 4863, TM+, aa*) 1-
(p, 863, FNM+, aa*) 1-
(p, 63, aNM+, aa*) 1-
(p, 63, NM+, aa*a) 1-
(p, 3, M+, aa*a) 1-
(p, e, +, aa*a) 1-
(p, e, e, aa*a+) 1-
C'est une configuration de «succès», et l'expression "a a * a +" est la forme
post-fixée de l'expression infixée "a* a+ a". Pour confirmation, il suffit de suivre
la courbe fléchée qui contourne l'arbre abstrait de l'expression infixée "a *a + a"
sur la figure suivante, et de marquer chaque nœud visité lorsqu'il se trouve à
gauche de la courbe.
Chapitre 4
Introduction à la compilation

La compilation embrasse plusieurs thématiques fondamentales de


l'informatique comme la théorie des langages, l'algorithmique, les structures
de données, les langages de programmation, l'architecture machine et le
génie logiciel. Dans ce chapitre on présente d'abord une vue globale du
processus de compilation en mettant particulièrement l'accent sur les
différentes phases d'un compilateur et leur enchainement. On décrit ensuite,
à la fin du chapitre, quelques outils et techniques d'analyse et de traduction.

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

Figure 58 : Schéma simplifié du fonctionnement d'un compilateur

Généralement le langage source est un langage de haut niveau (évolué) comme


les langages C, Java, Fortran, Ada, Pascal, Dynamo, Simula, Modula, etc. Le
langage cible est habituellement un langage d'assemblage ou un langage machine.
Il peut ne pas être un langage machine concret, mais un langage machine virtuel
(ou abstrait), voire même un langage évolué (compilation symbolique).

Lors du processus de compilation, un rôle important du compilateur est de


mentionner à l'utilisateur les erreurs détectées dans le programme source.

1.2 Classification des compilateurs


On classe parfois les compilateurs selon la fonction qu'ils sont censés réaliser ou la
manière dont ils ont été conçus. On rencontre des compilateurs multi-passes,
compilateurs en une seule passe, compilateurs-exécuteurs, compilateurs de mise au
point ou compilateurs optimisants, etc [Aho, 86].
Introduction à la compilation 125

L'existence de compilateurs multi-passes, par exemple, a pour ongme le


manque de ressources matérielles des ordinateurs. En effet, la compilation étant
un processus onéreux, et les premiers ordinateurs n'avaient pas suffisamment de
mémoire pour pouvoir supporter un programme devant réaliser ce travail.
Cependant, malgré cette apparente complexité, les tâches de base que tout
compilateur doit réaliser restent essentiellement les mêmes. Une fois ces tâches
maitrisées, on peut écrire des compilateurs pour toute une panoplie de langages de
programmation et de machines cibles en utilisant les mêmes techniques
fondamentales que l'on rencontre dans tout système traducteur.

1.3 Historique des compilateurs


Les difficultés rencontrées avant l'apparition des premiers compilateurs étaient
dues, entre autres, à l'absence de méthodes et de techniques de compilation, le
non-développement de la théorie des langages, l'inexistence d'un compilateur
souche, l'absence totale d'environnements de programmation adéquats et d'outils
d'aide au développement.
Les premiers compilateurs ont été écrits directement en langage assembleur, un
langage symbolique élémentaire correspondant aux instructions de la machine
cible et quelques structures de contrôle légèrement plus évoluées.
Les connaissances sur la façon d'organiser et d'écrire un compilateur se sont
largement développées depuis l'apparition des premiers compilateurs au début des
années 1950. On ne connait pas la date exacte de l'apparition du premier
compilateur parce qu'au départ plusieurs équipes ont conduit indépendamment
plusieurs expérimentations et réalisations. La plus grande part des travaux sur la
compilation était dédiée à la traduction de formules arithmétiques en code
machine (Aho, 86].
Tout au long des années 1950, les compilateurs furent considérés comme des
programmes pénibles à écrire. Il fallut 18 hommes-années d'efforts (un spécialiste
en 18 ans ou 18 spécialistes en 1 année), pour venir à bout des difficultés imposées
par la construction du premier compilateur comme Fortran 1. Mais, avec le
développement de la théorie des langages, des techniques de compilation et des
environnements de programmation particulièrement bien adaptés, la construction
d'un compilateur substantiel peut aujourd'hui faire l'objet d'un mini projet
(travail pratique) associé à un cours de compilation sur un semestre (Aho, 86].

1.4 Notion de bootstrapping (ou problème de l'amorçage)


En système d'exploitation, un bootstrap est un petit programme d'amorçage qui
permet d'en lancer un autre plus volumineux.
En compilation, un bootstrap est un compilateur écrit dans son propre langage.
En d'autres termes, le bootstrapping est une technique de construction de
compilateurs portables au cours de laquelle on va écrire le compilateur dans son
propre langage source.
Le problème est que si l'on doit utiliser un compilateur pour le langage Lo afin de
construire un autre compilateur pour le même langage Lo, comment alors le
premier compilateur a-t-il été créé ? Les techniques suivantes expliquent comment
on est parvenu à résoudre ce type de paradoxe.
126 Chapitre 4

On écrit un compilateur (ou un interpréteur) pour le langage Lo en langage Li.


Par exemple, le Fortran 1 est écrit en assembleur, Matlab, en Fortran, etc.
On peut utiliser un autre compilateur (ou interpréteur) pour le langage Lo qui
a déjà été écrit dans un langage Li. Le premier langage de haut niveau à
fournir un tel bootstrap était NELIAC en 1958.
Des versions initiales du compilateur sont écrites dans un sous-ensemble de Lo
pour lequel il existait déjà un autre compilateur. C'est ainsi que de nouvelles
versions Java ou c++ sont bootstrappées.
Le compilateur de Lo est issu d'une autre architecture pour laquelle il existe
déjà un compilateur pour L0 . C'est ainsi que l'on assure généralement la
portabilité de certains compilateurs vers d'autres plates-formes.
En écrivant le compilateur optimisant Lo, puis en le compilant à la main à
partir de son code source (probablement d'une manière non optimisée) et en
tournant le code obtenu pour obtenir le compilateur optimisant.
Les premiers compilateurs, comme on l'a si bien souligné ci-dessus, ont été
écrits en langage d'assemblage. Ce dernier doit être assemblé (et non compilé) et
lié (link-edité) pour obtenir une version exécutable. Un langage d'assemblage ou
langage assembleur est un langage de bas niveau qui représente le langage
machine sous une forme lisible par un humain. Les combinaisons de bits du
langage machine sont représentées par des symboles mnémoniques, c'est-à-dire
faciles à retenir. L'assembleur (le traducteur assembleur) convertit ces
mnémoniques en langage machine en vue de créer un code exécutable.
Avec l'évolution, les compilateurs actuels sont généralement écrits dans le langage
qu'ils doivent compiler. En somme, une étape décisive est franchie lorsque le
compilateur pour un langage est suffisamment complet pour se compiler lui-
même : il ne dépend plus alors d'un autre langage (même de l'assembleur) pour
être produit.

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

Editeurs syntaxiques (ou éditeurs structurels). Un éditeur syntaxique est


une sorte d'éditeur de texte qui prend en entrée une suite de commandes
spécifiant l'écriture d'un programme source. Il permet d'assister un utilisateur
dans l'écriture d'un programme. En effet, il permet de vérifier que le texte est
conforme à la syntaxe du langage source, il peut également fournir
automatiquement les mots-clés (si un utilisateur saisit, par exemple, un mot-clé
comme repeat, l'éditeur syntaxique lui renvoie systématiquement le mot-clé
until). On dit qu'un éditeur syntaxique est orienté langage, contrairement à un
éditeur de texte classique. En outre, il produit très souvent une forme
intermédiaire semblable à celle de la partie analyse d'un compilateur.

.-
/ '\.
y +
/

a
/
* \
b
"' 10

Figure 59: Arbre abstrait de l'affectation y := a* b + 10


Paragrapheurs. Un paragrapheur a pour finalité de lire un programme source
et de le reproduire sous une forme plus claire et bien structurée. Par exemple, les
commentaires peuvent apparaitre dans une police de caractères spéciale ; les
instructions peuvent apparaitre indentées selon leur niveau hiérarchique dans le
programme source ; les mots clés peuvent apparaitre sous une autre couleur que
celle des autres mots du texte source, etc.
Interprètes (ou interpréteurs). Contrairement au compilateur, un interprète
ne génère pas de code cible. Comme son nom l'indique, il interprète (exécute) lui-
même les instructions spécifiées par le programme source. Par exemple, pour une
instruction d'affectation, l'interpréteur doit générer en mémoire une forme
arborescente comme celle de la Figure 59, puis effectuer les opérations spécifiées
par les nœuds au fur et à mesure qu'il parcourt l'arbre.
Contrôleurs statiques (ou analyseurs sémantiques). Un contrôleur statique
est un analyseur spécial qui vérifie la cohérence, voire le sens des instructions d'un
programme. Il permet de déceler des erreurs sémantiques ou logiques comme par
exemple, la tentative d'affectation d'une variable de type caractère ou type
pointeur à une variable numérique, l'utilisation d'une variable non initialisée ou
indéfinie dans une expression, la découverte d'une condition de boucle non
conforme aux bornes supérieure et/ou inférieure requises, un appel de procédure
ou fonction avec des paramètres effectifs (réels) incompatibles avec les paramètres
formels, etc.
Si la plupart des compilateurs traduisent en général un code d'un langage de
programmation vers un autre, ce n'est pas le cas de tous les compilateurs. En
effet, il existe une variété de compilateurs sans rapport avec la programmation
conventionnelle, mais où les techniques fondamentales de compilation sont très
128 Chapitre 4

largement employées. Dans les exemples de logiciels suivants, la partie analyse


est tout-à-fait similaire à celle d'un compilateur conventionnel.
Systèmes de composition de documents. Un système de composition de
documents est un logiciel qui prend en entrée un fichier contenant du texte à
mettre en page, dont une partie correspond à des directives ou commandes
indiquant des figures, des tableaux, des formules mathématiques, du texte en
gras, des puces, des numérotations. Bref, tout un ensemble de commandes
utiles pour obtenir le format souhaité pour un document.
Par exemple, le logiciel LaTeX compile un code écrit dans le langage de
formatage de texte LaTeX, pour le convertir en un document écrit dans le format
de présentation souhaitée, par exemple, DVI, PDF, PostScript, etc.
Dans la Figure 60, le fichier codé en LaTeX noté (1), produit comme résultat
en sortie, la formule de la série numérique notée (2) de la même figure.

\ documentclass{ minimal}
(1) ----+ \ begin{document}
\[\sum _ {n=lY{ +\infty}\frac{1}{n n2}=\frac{\pi n2}{6}\l
\end {document}

+oo

(2) -----------~ L nl2


"""' =rr62
n=l

Figure 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

Compilateurs de silicium. Le langage source d'un compilateur de silicium est


proche de celui d'un compilateur conventionnel, à la différence que les
variables manipulées ne représentent pas des emplacements mémoire
ordinaires, mais des signaux logiques (0 ou 1) ou des groupes de signaux dans
des circuits de commutation. Son rôle est de passer automatiquement de la
description fonctionnelle d'un circuit logique à son implantation dans le
silicium.
Interprètes de requêtes. Un interprète de requêtes est un logiciel qui traduit
un prédicat (formules logiques), en commandes parcourant une base de
données pour y rechercher des enregistrements vérifiant ce prédicat.

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.

"squelette du code source d'un programme"


,____________i ____________,
: précompilateur :
'------------i------------'
code source d'un programme
,--------~--------­
: compilateur !
'--------i--------"

code cible en langage d'assemblage


i
,------------,
: assembleur :
1------~-----"

code machine translatable


i
r--é-di;~~;de-li~~~~~h~~~~~;-: - - - ~!~~~!~~e~~jtet
L ____________ r ___________ ..! translatables

"code machine absolu"

Figure 61 : Environnement de compilation d'un programme

Le code source d'un programme est généralement reparti dans plusieurs


fichiers. La tâche consistant à produire le code source, à présenter au compilateur,
est parfois confiée à un programme un peu spécial nommé précompilateur ou
préprocesseur. Ce dernier peut également réaliser diverses tâches telles que :
Définition de « macro-définitions » ou tout simplement « macros » qui sont
des abréviations utilisées pour remplacer des constructions plus longues comme
par exemple, les macros #define suivantes en langage C :
#define NB LIGNES 24
#define NB COLONNES 80
#define TAILLE TAB NB LIGNES* NB COLONNES

Inclusion de fichiers. Inclusion d'en-têtes dans le texte d'un programme


source. Par exemple, en langage C lorsque le préprocesseur rencontre
l'instruction #include <global.h>, il la remplace systématiquement par le
contenu du fichier global.h.
130 Chapitre 4

Préprocesseurs rationnels. Un préprocesseur rationnel permet d'ajouter de


nouvelles instructions n'existant pas à la base dans un langage de
programmation. Autrement dit, on peut faire bénéficier un langage ancien, de
constructions plus modernes. Par exemple, si on dispose d'un compilateur du
langage Fortran, on peut structurer ce dernier en introduisant de nouvelles
instructions de contrôle comme if then else ou while, et en éliminant les
instructions mal structurées du genre goto calculé. A cet effet, au lieu de
construire un nouveau compilateur pour le langage Fortran structuré, on peut
utiliser l'ancien compilateur de la manière suivante : Ecrire un traducteur
(préprocesseur) qui traduira toutes les instructions structurées nouvellement
ajoutées en des instructions non structurées de l'ancien compilateur. Après la
pré-compilation, le programme obtenu pourra finalement être compilé sur
l'ancien compilateur Fortran.

Extension de langages. Comme son nom l'indique, ce genre de préprocesseur


permet d'ajouter de nouvelles fonctionnalités au langage par le biais de macros.
Par exemple, en langage C les instructions commençant par ## sont des
instructions du langage Equel qui est un langage de requêtes de base de
données. Ces instructions sont transformées par le préprocesseur en appels de
procédures qui effectuent ces accès.

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}

L'analyse syntaxique (parsing en Anglais), comme son nom l'indique, permet


d'identifier la structure syntaxique du programme. Cette phase s'appuie
généralement sur la construction d'un arbre d'analyse (ou arbre syntaxique).
Les tokens y sont regroupés et imbriqués hiérarchiquement selon la grammaire
formelle qui définit la syntaxe du langage. L'arbre syntaxique de la Figure 62
décrit la structure syntaxique d'une instruction d'affectation. Une
représentation interne plus compacte et plus usuelle de la même instruction est
illustrée par l'arbre abstrait de la Figure 63.
Un arbre abstrait est une représentation compacte de l'arbre d'analyse, dans
laquelle les opérateurs sont des nœuds internes et les opérandes sont des feuilles
ou des sous-arbres. Un sous-arbre ici a pour racine un nœud interne, c'est-à-dire
un opérateur.

<affectation>

<idf>
1
y
----- ---- .- /
/
<exp>
\------
<exp> + <exp>
/ / "-.... 1
<exp>
* <exp> <nbr>

<idf>
1
<i~f> 1
10
1 1
a b

Figure 62: Arbre syntaxique pour y:= a* b + 10


132 Chapitre 4

.-
/ '\.
y +
/ '\
/
* \
10

a b

Figure 63: Arbre abstrait pour y:= a* b + 10


L'analyse sémantique est la phase durant laquelle le compilateur doit s'assurer
que l'agencement des constituants du programme source a un sens, et récolter
les informations nécessaires à la génération du code final. Pour ce faire,
l'analyseur sémantique s'appuie sur la structure hiérarchique définie par la
phase d'analyse syntaxique afin d'identifier les opérateurs et les opérandes des
expressions ainsi que les instructions.
Une opération clé qui caractérise l'analyse sémantique est la vérification de
type. Le compilateur doit interdire des opérations non conformes aux
spécifications du langage, comme par exemple :
l'affectation d'une variable de type caractère ou type pointeur à une
variable numérique ;
l'utilisation d'une variable non initialisée ou indéfinie dans une expression,
l'utilisation de boucle non conforme aux bornes supérieure et/ou inférieure
requises ;
l'utilisation d'un nombre réel pour indicer un tableau ;
un appel de procédure ou fonction avec des paramètres effectifs (réels)
incompatibles avec les paramètres formels, etc.
Toutefois, la spécification du langage peut permettre certaines coercitions
d'opérandes comme, par exemple, le cas d'un opérateur arithmétique binaire qui
est appliqué à un entier et un réel. Dans un tel cas, le compilateur peut avoir à
convertir l'entier en réel.
L'analyse sémantique nécessite habituellement un arbre d'analyse complet ; ce
qui signifie que cette phase fait suite à la phase d'analyse syntaxique, et précède
logiquement la phase de génération de code ; mais il est possible de regrouper
toutes ces phases en une seule passe.
A l'issue des phases d'analyse (lexicale, syntaxique et sémantique), certains
compilateurs construisent explicitement une forme intermédiaire. Cette dernière
est considérée comme un programme pour une machine abstraite. Il existe une
variété de formes intermédiaires abstraites dont les plus répandues sont, l'arbre
abstrait, la forme polonaise inverse (ou la forme post-fixée) et le code à trois
adresses. L'arbre abstrait étant la représentation la plus générale. Il peut être
interprété sur n'importe quel type de machine. Le code à trois adresses est
semblable au langage d'assemblage d'une machine dans laquelle chaque
emplacement mémoire peut jouer le rôle d'un registre. Ce type de code s'adapte
donc à une machine à registres contrairement à la forme post-fixée, qui elle,
Introduction à la compilation 133

s'adapte plutôt à une machine à pile. On peut toutefois, au besoin, simuler le


comportement d'une machine à pile sur une machine à registres pour traiter du
code post-fixé.

L'optimisation de code est située à mi-chemin entre la génération de code


intermédiaire et la génération de code cible. Il existe deux types d'optimisation
de code : l'optimisation dépendante de la machine et l'optimisation
indépendante de la machine. Le premier type tente d'améliorer le code
intermédiaire sans prendre en considération les propriétés de la machine cible.
Le deuxième type, par contre, tient compte de la machine, particulièrement,
sur l'allocation des registres et l'utilisation de séquences d'instructions
spécifiques afin que le code machine résultant s'exécute plus rapidement. Par
exemple, dans le cadre de l'optimisation indépendante de la machine,
l'instruction d'affectation A := B + C * 10, a pour forme intermédiaire la
séquence de codes suivante :
Tl := Convert _Real (10)
T2 := C *Tl
T3 := B * T2
A:= T3
alors qu'avec un optimiseur de code, la séquence obtenue est la suivante :
Tl:= C * 10.0
A:= B +Tl

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.

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 donnera sous peu, quelques informations sur l'assembleur,
l'éditeur de lien et le chargeur.

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

Avant de parler des autres programmes de l'environnement du compilateur, il


convient de donner un exemple de traitement d'une instruction par un
compilateur pour faire un tour d'horizon des différentes phases sus-mentionnées.

Soit alors à compiler l'instruction d'affectation y : = a * b + 10.


On présentera chaque phase avec son flot d'entrée, son flot de sortie, ainsi que
l'interaction avec la table des symboles lorsque cela est nécessaire. La chaine de
traitement de cette instruction est illustrée par la Figure 64.

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 [+]

multRéel [*] / ~t-réel


/~
idf2 i idfs 10

générateur de code intermédiaire


-}
Tl := idf2 * idfs
T2 := ent-réel (10)
T3 :=Tl+ T2
Idf1 := T3
-}

optimiseur de code , ___ Table des symboles


-}

Tl := idf2 * idfs
idft := Tl + 10.0
-}
générateur de code final

MOVF idf2, RO charger idf2 dans RO


MULF idfs, RO multiplier RO par idfs et charger le résultat dans RO
ADDF #10.0, RO ajouter 10.0 à RO et charger le résultat dans RO
MOVF RO, id.f1 ranger RO à l'adresse mémoire id.f1

Figure 64 : Compilation d'une instruction d'affectation


Pour simplifier l'illustration, on a supposé que la structure générée par
l'analyseur syntaxique est représentée par l'arbre abstrait associé. En pratique, ce
dernier constitue une forme intermédiaire suffisante pour la génération de code
cible. Mais, pour des raisons de clarté, on a préféré présenter d'une autre manière
le processus en supposant que le compilateur produit d'abord un code à trois
adresses qui se prête particulièrement bien à la traduction en langage
d'assemblage d'une machine à registres.
Concernant l'interaction avec la table des symboles, on peut voir que cette
dernière a été initialisée par les noms des identificateurs de l'instruction
d'affectation, représentés par les variables y, a et b. Au cours des phases
136 Chapitre 4

suivantes, cette table est actualisée progressivement au fur et à mesure que le


processus de compilation avance. En effet, par exemple les identificateurs y, a et b
ont commencé à être mis à jour par l'analyse syntaxique en leur associant leurs
types et adresses respectifs. D'autres informations, relatives à ces entités, peuvent
être ajoutées à la table des symboles, si c'est nécessaire, au cours des phases
ultérieures.
T 1 , T 2 et T 3 sont des variables temporaires créés automatiquement par le
compilateur lui-même ; mais elles doivent apparaitre comme tout autre variable
(y, a et b) au niveau de la table des symboles. Le 11 F 11 du code opération de
chaque instruction signifie que celle-ci manipule des nombres en virgule flottante.
Une structure de données d'implantation type, de l'arbre abstrait associé à
l'instruction d'affectation est illustrée par le schéma de la Figure 65.

10
pointeur vers la *
table des symboles b
pour la variable y

pointeur vers la table des


pointeur vers la table des symboles
symboles pour la variable b
pour la variable a

Figure 65 : Exemple de structure de données d'implantation de l'arbre


abstrait de l'instruction d'affectation y := a* b + 10

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

Lors de la seconde passe : relecture de la séquence de code et traduction de chaque


instruction en la suite de bits correspondante. En fait, chaque code opération se
voit attribuer la suite de bits qui le représente dans le langage de la machine, et
chaque identificateur sera remplacé par la séquence de bits représentant son
adresse dans la table des symboles. Les suites de bits obtenues constituent
finalement du code machine translatable, c'est-à-dire qui peut être chargé en
mémoire à partir de n'importe quelle adresse. Par exemple, si cette adresse est X,
on doit ajouter X à toutes les adresses dans le code pour que toutes les références
soient correctes. En d'autres termes, l'assembleur doit mettre en valeur toutes les
instructions qui référencent des adresses qui peuvent être translatées.

IDENTIFICATEUR ADRESSE
X 0
y 4

Tableau XIII- Table des symboles d'un assembleur contenant les


identificateurs x et y

Soit alors une machine hypothétique dont le code correspond à la


traduction de la séquence de code assembleur de l'affectation y:= x + 10. Ce
code est le suivant :
0001 01 OO 00000000*
0011 01 10 00000010
0010 01 OO 00001010*
Les quatre premiers bits du code, à savoir, 0001, 0011, 0010 signifient
respectivement :
charger (de mémoire vers le registre),
additionner puis charger le résultat dans un registre,
ranger (de registre vers la mémoire).

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

Le relieur-chargeur (éditeur de liens et chargeur)


On doit faire la distinction entre un relieur (ou éditeur de liens) et un chargeur ;
chacun d'eux assure sa propre fonction. Mais, ils peuvent généralement être
combinés en un seul programme nommé chargeur.
Le chargement translate les adresses translatables, et range en mémoire, après
modification, les instructions et les données aux adresses adéquates.

Quant au relieur (ou éditeur de liens), il permet de former un seul programme


à partir de plusieurs fichiers contenant du code machine translatable. Ces
fichiers peuvent avoir été compilés séparément ou provenant des bibliothèques
du système. Mais, s'ils doivent être employés ensemble, il faut résoudre les
références externes. On parle de référence externe lorsque le code d'un fichier
fait référence à une adresse se trouvant dans un autre fichier. Autrement dit,
l'adresse est définie dans un fichier et utilisée dans un autre. Par exemple, si
un autre fichier référençant l'identificateur y est chargé en même temps que
celui contenant le code translatable de l'instruction y:= x + 10, on doit
remplacer cette référence par 4 à laquelle on ajoute l'adresse d'implantation ou
de chargement du code translatable de l'instruction y:= x + 10.

4 Regroupement des différentes phases d'un compilateur


A l'origine, c'est le manque de ressources matérielles des ordinateurs qui a poussé
les concepteurs à diviser le compilateur en sous-programmes qui font chacun une
lecture de la source pour accomplir les différentes phases du processus de
compilation.
Ces différentes phases sont souvent réunies en une partie avant (frontale) et
une partie arrière (finale).
La partie avant comprend les phases d'analyse (lexicale, syntaxique et
sémantique), de production de code intermédiaire, de gestion de la table des
symboles, de traitement des erreurs, et une partie de l'optimisation de code.
Ces phases dépendent principalement du langage source, mais elles sont
indépendantes de la machine cible. La génération de code intermédiaire dépend
du langage dans lequel sera codée la forme intermédiaire du programme source.

La partie arrière est constituée des phases de génération de code cible, de


l'optimisation de code, du traitement des erreurs et de la gestion de la table
des symboles. A l'inverse de la partie avant, la partie arrière ne dépend
généralement pas du langage source, mais uniquement du langage
intermédiaire et des caractéristiques de la machine cible.

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

Parallèlement à ces tâches, l'analyseur syntaxique fait également appel au


générateur de code intermédiaire pour qu'il vérifie le sens d'une construction et
générer le code intermédiaire correspondant.
Le fait de regrouper l'ensemble des phases d'un compilateur en une seule passe
a été considérée évidemment comme un avantage, car cela simplifie la tâche
d'écriture d'un compilateur, et il compile généralement plus rapidement qu'un
compilateur multi-passe. Ainsi, à cause des ressources limitées des premiers
systèmes, de nombreux langages ont été spécifiquement conçus afin qu'ils puissent
être compilés en un seul passage (par exemple, le langage Pascal). En
contrepartie, si l'on regroupe trop de phases en une seule passe, on peut être
contraint de conserver le programme tout entier en mémoire, parce que l'une des
phases peut avoir besoin d'informations dans un ordre autre que celui dans lequel
la phase précédente les fournit.
Inversement, ce n'est pas toujours possible de générer du code cible tant qu'on
n'a pas terminé avec succès la production de code intermédiaire. En effet, il peut
arriver que la conception d'une fonctionnalité de langage ait besoin d'un
compilateur qui doit effectuer nécessairement plus d'une passe. Par exemple, une
déclaration apparaissant à la ligne N d'une source, affecte la traduction d'une
déclaration figurant à la ligne N-10. Dans ce cas, la première passe doit recueillir
des informations sur les déclarations situées après les déclarations qu'ils affectent,
avec la traduction, proprement dite, qui s'effectue lors d'un passage ultérieur.
Certains langages permettent l'utilisation de variables avant leur déclaration, mais
cela ne permet pas d'obtenir le code cible pour une construction si l'on ne connait
pas le type des variables mises en jeu dans la construction en question.
Dans le même contexte, plusieurs autres langages autorisent des branchements
dont la destination n'est pas encore définie. On ne pourra connaitre l'adresse de
branchement qu'une fois qu'on a rencontré sa définition dans le programme
source, et généré le code cible approprié. Une solution généralement préconisée
pour déterminer l'adresse d'un tel branchement est l'utilisation de la technique du
« trou » laissé dans le code pour toute information non encore connue, et la mise-
â-jour de ce code (remplir ce trou) lorsque l'information concernant ce trou
devient disponible. Ainsi, si l'on rencontre dans un programme source une
instruction de branchement GOTO X, avant d'avoir défini l'adresse X, on génère
temporairement le code BR (code du branchement) tout en laissant le champ
d'adresse à blanc (trou). S'il existe plusieurs instructions du même type, on garde
leurs adresses respectives dans une liste ; mais à la rencontre de l'instruction de
destination d'étiquette X, on effectue une « reprise arrière » (Backpatching), c'est-
â-dire qu'on parcourt la liste associée à X et, pour chaque instruction de cette
liste, on met à jour les champs laissés à blanc (les trous) par l'adresse de
l'instruction étiquetée par X.

5 Compilation et interprétation, quelles différences ?


Un interpréteur est un programme qui exécute directement des programmes. En
d'autres termes, le processus d'interprétation repose sur une analyse dynamique
des données sur lesquelles les programmes s'exécutent.
140 Chapitre 4

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).

code source d'un programme

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

Figure 66 : Différentes phases de compilation d'un programme

Dans ce sens général, la compilation constitue un investissement, qui peut être


onéreux, mais bénéfique. En effet, le travail statique n'étant réalisé qu'une seule
fois pour un nombre potentiellement infini d'exécutions différentes.
Inversement, l'interprétation est un mode d'exécution très rapide à mettre en
œuvre, mais souvent peu efficace. En effet, beaucoup de traitements étant
repoussés à l'exécution qui est généralement plus lente que l'exécution du même
programme compilé.
De ce fait, la compilation parait idéale pour les programmes en phase
d'exploitation, contrairement à l'interprétation qui est plutôt préférée lors des
phases de dév-eloppement et de mise au point.
Les schémas décrivant les différentes phases d'un compilateur et d'un
interpréteur sont présentés respectivement dans les Figures 66 et 67. L'examen
de ces figures montre qu'aussi bien le compilateur que l'interpréteur, débutent par
les mêmes phases d'analyse lexicale et syntaxique ; la différence réside dans la
manière avec laquelle ces phases sont appliquées.
Introduction à la compilation 141

Dans le compilateur, l'analyseur lexical étant en général une coroutine de


l'analyse syntaxique ; l'analyse syntaxique est donc appliquée en une seule
passe au programme source tout entier. Dans le cas d'un compilateur multi-
passe comme celui de la Figure 66, l'analyse lexicale est d'abord appliquée au
programme source tout entier. Ensuite, c'est au tour de l'analyse syntaxique
de prendre le relai pour construire la structure hiérarchique associée au
programme source. Le flot d'entrée de l'analyse syntaxique correspond
évidemment au code intermédiaire généré par l'analyse lexicale.

programme
source
i
t instruction (ou expression) textuelle

analyseur lexical et syntaxique


·----------------------------.
! gestionnaire de la ; instruction (ou expression)
r---~~~ti~~~~;~~---;
!:____________________________
table de symboles !!
l_____~:~~~~1:1!_~ _____
textuelle structurée j

i
évaluation

i
résultat

Figure 67 : Phases d'un interpréteur

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

L'approche la plus récente pour créer des applications multi-plateformes est


l'utilisation de langages semi-interprétés. Ces langages sont compilés vers un code
intermédiaire qui est interprété. Cette approche présente les mêmes avantages que
les codes interprétés mais possède une plus grande vitesse d'exécution. Le
principal exemple est le langage Java de Sun Microsystems. Ce langage est
compilé en bytecode Java qui est ensuite interprété par une machine virtuelle. Le
bytecode peut être exécuté sur chaque plate-forme où la machine virtuelle est
implémentée.
En bref, le principe d'un interpréteur mixte consiste en les points suivants :
Définition d'un langage intermédiaire et d'une machine virtuelle capable
d'interpréter ce langage.
Ecriture d'un compilateur du langage source vers le langage intermédiaire
envisagé.
Ecriture d'un interpréteur du langage intermédiaire, c'est-à-dire un simulateur
de la machine virtuelle.
Il faut noter qu'on parle parfois de langages compilés ou interprétés. En effet,
le caractère compilé ou interprété ne dépend pas du langage, qui n'est finalement
qu'un ensemble de symboles avec une certaine sémantique. D'ailleurs, certains
langages peuvent être utilisés interprétés ou compilés. Par exemple, il est très
courant d'utiliser Lisp avec un interprète, mais il existe également des
compilateurs pour ce langage. Néanmoins, l'usage qu'on fait des langages est
généralement fixé.
Enfin, pour clore cette section, le Tableau XIV résume quelques
caractéristiques essentielles concernant un compilateur et un interpréteur.

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

Tableau XIV- Compilation et interprétation: comparaison


Introduction à la compilation 143

6 Outils d'aide à la construction de compilateurs


Avec l'évolution des systèmes et le développement des environnements de
développements, la production de compilateurs est passée du stade artisanal au
stade industriel. En effet, aujourd'hui notamment avec le développement du génie
logiciel et l'existence de nombreux AGL (Ateliers de Génie Logiciel), la
construction de compilateurs se résume à quelques mois, voire même parfois, à
quelques semaines.
Les outils les plus populaires parmi les générateurs d'analyseurs lexicaux et
syntaxiques et qui sont devenus des normes dans le domaine de la conception et
de la construction de compilateurs sont les outils Lex (ou Flex) et Yacc (ou
Bison) qui sont connus pour leur efficacité, en particulier, dans la construction
d'analyseurs lexicaux et d'analyseurs syntaxiques.

Tout comme les compilateurs classiques, un générateur d'analyseurs, reçoit en


entrée un langage source, par exemple une grammaire avec des actions, et produit
en sortie un langage cible, le plus souvent des portions d'analyse lexicale et
syntaxique. Les parties d 1analyse sémantique et de synthèse sont généralement
trop proches du langage cible pour être produites automatiquement, et leur
réalisation est laissée à la charge de l'utilisateur. Certains générateurs d'analyseurs
permettent de créer également une partie du gestionnaire des erreurs. D'autres,
peuvent produire du code pour une machine cible, mais à partir d'un langage
intermédiaire ; on les nomme générateurs de code automatiques.
On cite ci-après quelques outils utiles à la construction de compilateurs [Aho, 86] :

Constructeurs d'analyseurs lexicaux. Ce sont des générateurs automatiques


d'analyseurs lexicaux. La construction de ce type d'analyseur s'appuie
généralement sur une spécification basée sur des expressions régulières. Le
résultat en sortie est un analyseur lexical basé sur un automate d'états finis
équivalent aux expressions régulières présentées en entrée. Exemple classique
et bien connu : Lex (ou Flex).
Constructeur d'analyseurs syntaxiques. Ce sont des outils permettant la
construction automatique d'analyseurs syntaxiques, à partir d'une spécification
basée sur une grammaire à contexte libre. Auparavant avant le développement
des techniques de compilation, l'analyse syntaxique demandait non seulement
une part importante du temps d'exécution du compilateur, mais aussi une part
substantielle de l'effort intellectuel déployé pour venir à bout des difficultés
imposées par la construction du compilateur. Exemple classique et bien
connu : Yacc (ou Bison).
Moteurs de traduction dirigée par la syntaxe. Ces moteurs sont des
programmes qui produisent des ensembles de procédures qui parcourent l'arbre
syntaxique en générant parallèlement du code intermédiaire. L'idée sous-
jacente est qu'à chaque nœud de l'arbre syntaxique on associe une ou plusieurs
traductions, et que chaque traduction est définie en termes des traductions
associées aux nœuds voisins dans l'arbre. On entend par voisinage d'un nœud,
un sous-ensemble constitué des nœuds frères situés à sa droite, et de ses nœuds
descendants directs (fils).
144 Chapitre 4

Générateurs de code automatiques. Ils reçoivent en entrée une collection de


règles sémantiques qui définissent pour chaque opération du langage
intermédiaire, sa traduction en code de la machine cible.

7 Notions fondamentales d'analyse et de traduction


7.1 Rappels
Pour décrire des lexèmes (mots-clés, identificateurs, constantes, etc.) on utilise
généralement des expressions régulières qui sont des modèles concis de
spécification.

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.

Figure 68 : Diagramme de transition des entiers naturels

Les identificateurs de longueur ::;; 3 peuvent être représentés par :


• l'expression régulière: l•(c ® 1)+ 2 avec 1 e {a, b, ... z} etc e {O, 1, ... 9}
• la grammaire : S ~ 1 / 1 A ; A ~ c / 1 / c B / 1 B ; B ~ c / 1
• l'automate fini, dont l'ensemble des états est {s, r, p, f} et l'ensemble des
états finals est {r, p, f}, qui est graphiquement illustré par les Figures 69
et 70.
La contrainte sur la longueur (:::; 3) peut être réexaminée et exprimée par une
routine de contrôle de la longueur en utilisant un automate plus simple comme
celui de la Figure 70. Ce dernier peut reconnaitre des identificateurs de
n'importe quelle longueur. Il suffit donc, au cours de la reconnaissance,
d'ajouter un contrôle sur la longueur du lexème à analyser.

c,l Ai
~y

Figure 69 : Diagramme de transition des identificateurs de longueur S 3


Introduction à la compilation 145

c, 1

Figure 70 : Diagramme de transition des identificateurs de longueur


quelconque

Quant à la description des instructions d'un langage de programmation on


utilise souvent des grammaires à contexte libre sous forme BNF ou EBNF ou sous
forme de diagrammes syntaxiques et, bien sûr, aussi sous forme conventionnelle.

Pour la reconnaissance de ces langages on utilise, évidemment, dans la plupart


des cas, les grammaires à contexte libre qui sont à la fois des outils de
spécification et d'analyse.

Pour implémenter un interpréteur pour un besoin particulier, on préfère


parfois utiliser les automates à pile, vu leur efficacité et leur rapidité.

Avant de définir la traduction dirigée par la syntaxe, il convient de rappeler


d'abord les notions de dérivation et d'arborescence (arbre syntaxique et arbre
abstrait).

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

Arbre syntaxique et arbre abstrait


Un arbre syntaxique (on dit aussi arbre de dérivation et parfois arbre d'analyse),
est une autre façon d'exprimer une dérivation gauche ou droite.
Le mot 11 6 - 1 + 7 11 a pour arbre syntaxique l'arbre de la Figure 71.
L'arbre abstrait est une représentation compacte de l'arbre syntaxique, dans
laquelle les opérateurs sont des nœuds internes, et les opérandes sont les fils du
nœud opérateur en question. L'arbre abstrait correspondant à l'arbre de
dérivation de l'expression de la Figure 71 est illustré par la Figure 72.

E
T

6 1 + 7

Figure 71 : Arbre syntaxique de l'expression 11 6 - 1 + 7 11

~+~
~-~ 7
6 1

Figure 72 : Arbre abstrait de l'expression 11 6 - 1 + 7 11

Si on avait utilisé la grammaire ambiguë définie par S ~ S + S 1 S - S 1 c,


l'expression 11 6 - 1 + 711 aurait donné deux arbres (ambiguïté) comme (1) et (2)
de la Figure 73.

(1) s (2) ~

(Îî
s

6 1 + 7 6 1 + 7

Figure 73 : Deux arbres syntaxiques distincts pour la même expression


11 6- 1 +7 11

Avec l'arbre (1), l'expression 6 - 1 + 7 est équivalente à (6 - 1) + 7, c'est-à-dire


sa valeur est égale à 12 ; avec l'arbre (2) elle est équivalente à 6 - (1 + 7), c'est-à-
Introduction à la compilation 147

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.

7.2 Traduction dirigée par la syntaxe


Notation post-fixée
La notation post-fixée d'une expression est définie comme suit :
Si E correspond à une unité atomique (une variable ou une constante), la
notation post-fixée de E est le terme E lui-même.
148 Chapitre 4

Si E = Ei 'Ô E2 où 'Ô est un opérateur binaire, alors la notation post-fixée de


l'expression E est définie par E' = E'i E'2 'Ô, avec E'1 et E'2 qui sont les
notations post-fixées respectivement des expressions E 1 et E2.
Si E = 'Ô E 1 où 'Ô est un opérateur unaire (ou monadique), alors E' = E' 1 -ô,
avec E'1 qui est la notation post-fixée de Ei.
Si E = (E1), alors l'expression post-fixée de E est notée E' = E'i. L'expression
E'1 est la notation post-fixée de l'expression E 1.
Par exemple, l'expression 6 + 1 / 7, a pour représentation post-fixée 6 1 7 / +.
En effet, en appliquant le formalisme précédent on a: 6+1/7 qui donne d'abord 6'
(1/7)'+ qui est égale à 6 1' 7' / +, laquelle produit finalement 6 1 7 / +, qui
correspond au résultat escompté.
On peut également obtenir la notation post-fixée de n'importe quelle
expression en utilisant la stratégie qui consiste à parcourir l'arbre abstrait de
l'expression et marquer (imprimer) chaque nœud visité lorsqu'il se trouve à
gauche du contour de l'arbre. L'arbre de la Figure 14 correspond à l'arbre
abstrait ·de l'expression 6 + 1 / 7. Son parcours en post-ordre, c'est-à-dire en
profondeur de gauche à droite, en marquant chaque nœud visité, comme prévu,
produit effectivement la forme post-fixée 6 1 7 / + de l'expression 6 + 1 / 7.

Figure 74 : Parcours en post-ordre de l'arbre abstrait de l'expression


11 6 +1/ 7"

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.

De même, tout comme avec la forme post-fixée, on peut également utiliser le


parcours de l'arbre abstrait de l'expression a * b + c, mais en préordre, c'est-à-
dire un parcours en profondeur de gauche à droite, en marquant chaque nœud
visité lorsqu'il se trouve à droite du contour de l'arbre. L'arbre de la Figure 75
correspond à l'arbre abstrait de l'expression a * b + c. Son parcours en préordre,
c'est-à-dire en profondeur de gauche à droite, en marquant chaque nœud visité,
comme préconisé, produit effectivement la forme préfixée escomptée, de
l'expression a * b + c.
Introduction à la compilation 149

Définition dirigée par la syntaxe


Etant donnée une grammaire à contexte libre.
A chaque production on associe un ensemble de règles sémantiques ;
La grammaire et l'ensemble des règles sémantiques constituent la définition
dirigée par la syntaxe ;
Une traduction sur la base de cette définition dirigée par la syntaxe est une
correspondance entre un texte d'entrée et un texte de sortie.

Figure 75 : Parcours de l'arbre abstrait de l'expression "a* b + c"


On donne ci-après, Tableau XV, un exemple de définition dirigée par la
syntaxe pour traduire une expression arithmétique infixée en sa correspondante
préfixée :

Productions N° règles Règles sémantiques


E--1E+T (1) +ET
E--1T (2) T
T--1T*F (3) *TF
T--1F (4) F
F --1 a (5) a
F --1 (E) (6) E

Tableau XV- Définition dirigée par la syntaxe pour la traduction des


expressions arithmétiques en leurs correspondantes préfixées

L'arbre abstrait de l'expression a * a est illustré par la Figure 76.


En adoptant la stratégie du parcours en préordre de cet arbre, on obtient la forme
préfixée de l'expression a * a, c'est-à-dire * a a.
Pour confirmation, on peut utiliser la définition dirigée par la syntaxe comme
suit :
E =>( 2) T =>( 3) T * F =>(4) F * F =>( 5) a * F =>( 5) a * a. On a donc 7t1 = 2 3 4 5 5, la
dérivation canonique qui sera utilisée pour appliquer les règles sémantiques
appropriées. Ainsi, en combinant les numéros des règles de la dérivation
canonique 7t1 = 2 3 4 5 5 avec les règles sémantiques correspondantes, on obtient
la forme préfixée envisagée : E =>( 2) T =>( 3 ) * T F =>( 4 ) * F F =>( 5 ) * a F =>( 5)
*a a.
150 Chapitre 4

Figure 76 : Parcours en préordre de l'arbre abstrait de l'expression "a* a"

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.

On note la concaténation des chaines dans les règles sémantiques par le


symbole « 11 ».

L'exemple suivant illustre une définition dirigée par la syntaxe permettant de


traduire toute expression formée de chiffres séparés par des signes ( + ou -) en sa
représentation post-fixée.

Productions Règles sémantiques


E ~ Ei + T E.t := Ei.t Il T.t Il '+'
E ~ Ei -T E.t := Ei.t 11 T.t 11 ,_,
E~T E.t := T.t
T~O T.t := 'O'
T~l T.t := '1'
... ...
T~9 T.t := '9'

Tableau XVI- Définition dirigée par la syntaxe pour la traduction des


expressions arithmétiques en leurs correspondantes post-fixées

En appliquant, à l'arbre syntaxique de l'expression 6 - 1 + 7, les règles


sémantiques appropriées, on obtient l'arbre décoré et annoté correspondant.
L'arbre syntaxique de l'expression est présenté par la Figure 77.
Introduction à la compilation 151

E
T

6 1 +
Figure 77: Arbre syntaxique de l'expression 11 6 - 1 + 7"

Pour calculer la valeur d'un attribut on utilise la notion d'attribut synthétisé


ou d'attribut hérité. Dans le cas présent, on s'appuie sur les attributs synthétisés.
Un attribut est dit synthétisé si sa valeur à un nœud est déterminée à partir
des valeurs d'attributs de ses fils en appliquant les règles sémantiques appropriées.
En vertu de cette règle de calcul fondée sur les attributs synthétisés, on peut
déduire à partir d'un arbre syntaxique l'arbre synthétisé (décoré et annoté)
correspondant. La Figure 78 présente l'arbre synthétisé associé à l'arbre
syntaxique de la Figure 77.

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"

Parcours en profondeur de l'arbre syntaxique


Le parcours en profondeur se définit de manière récursive sur l'arbre. Le parcours
consiste à traiter la racine de l'arbre et à parcourir récursivement les sous-arbres
de la racine. L'algorithme général de parcours en profondeur, comme on peut le
voir sur la Figure 79 démarre à la racine en s'appelant récursivement pour les fils
de chaque nœud dans un ordre allant de gauche à droite.
Dans le cadre de l'analyse et de la traduction de l'expression 11 6 - 1 + 7", la
Figure 80 illustre comment, et à quel niveau sont appliquées les règles
sémantiques de la définition dirigée par la syntaxe au cours de l'algorithme de
parcours de la Figure 79. Dans la Figure 80, les flèches en trait continu
indiquent un appel de procédure, tandis que les flèches en pointillés indiquent un
retour de procédure. Aussi bien dans la Figure 78, que dans la Figure 80, la
152 Chapitre 4

valeur de l'attribut à la racine est E.t =6 1- 7 + qui est la forme post-fixée de


l'expression 6 - 1 + 7.

procédure visiter(X : nœud) ;


début
pour chaque fils Y de X en partant de la gauche vers la droite
faire
visiter (Y) ;
1
appliquer les règles sémantiques au nœud X
fin

Figure 79: Algorithme général de parcours en profondeur d'un arbre

E.t = 6 1- 7+ E.t = 6 1- E.t = 6 1- E.t = 6 T.t =6


T--+ 6

~ ~\~
•.,.. 1 -
E.t =6 T.t =6
~ \, T.t =1
'1' ~ 7 T-+ 1

Figure 80 : Calcul des attributs par application des règles sémantiques au


cours du parcours en profondeur: cas de l'expression 11 6 - 1 + 7 11

La définition dirigée par la syntaxe n'impose aucun ordre particulier pour


l'évaluation des attributs sur l'arbre syntaxique. Tout ordre qui calcule un
attribut 11 t 11 après tous les attributs dont "t" dépend est acceptable.
Schéma de traduction dirigée par la syntaxe
Un schéma de traduction correspond à une grammaire à contexte libre décorée par
des actions sémantiques à l'intérieur même des membres droits des règles de
production.
Contrairement à la définition dirigée par la syntaxe qui n'impose aucun ordre
spécifique, l'ordre d'évaluation dans un schéma de traduction est explicitement
fixé : on place entre accolades à l'endroit approprié, l'action sémantique à
exécuter. Par exemple, dans le cadre de la traduction d'une expression infixée en
sa correspondante post-fixée, pour la règle T---7 *FN, l'action sémantique est
insérée après le non-terminal F, ce qui donne le schéma de traduction
T---7 *F {Imprimer ('*')} N, c'est-à-dire que, l'action sémantique entre accolades
ne sera exécutée qu'une fois que le sous-arbre de racine F aura été parcouru. En
d'autres termes, si on suppose que F est une procédure appelée à partir de T,
alors on empile le symbole *, et au retour, on le dépile, puis on l'imprime avant
d'appeler la nouvelle procédure N.
Ainsi, dans la construction de l'arbre syntaxique associé au schéma de
traduction, on peut spécifier l'action sémantique à l'endroit approprié en la reliant
Introduction à la compilation 153

par des pointillés au nœud correspondant à la partie gauche de la production. La


Figure 81 illustre le schéma de traduction T ~ *F {Imprimer ('*')} N sous forme
arborescente.

T
*
Figure 81 : Arbre syntaxique d'un schéma de traduction

Comparaison des deux modèles de traduction


L'une des différences entre les deux modèles de traduction est, comme annoncé
précédemment, réside dans le fait qu'une définition dirigée par la syntaxe
n'impose aucun ordre dévaluation, alors que dans un schéma de traduction l'ordre
d'évaluation est préétabli. Une définition dirigée par la syntaxe est une
spécification de haut niveau pour les traductions. Elle cache de nombreux détails
d'implantation et évite à l'utilisateur d'avoir à spécifier l'ordre dans lequel la
traduction s'effectue. Le schéma de traduction, par contre lui, permet de mettre
en valeur certains détails d'implantation, en explicitant l'ordre d'exécution des
actions sémantiques.
D'un point de vue fonctionnement, quand on traduit, par un schéma de
traduction, par exemple, l'expression 6 - 1 + 7 en 6 1 - 7 +, on émet (imprime)
directement en sortie, chaque caractère de l'expression analysée sans utiliser le
moindre fichier intermédiaire. Ceci dit, donc l'ordre d'émission des caractères en
sortie est important.
Dans la définition dirigée par la syntaxe, en revanche, la traduction est
exprimée formellement par des règles sémantiques, comme le montrent les deux
exemples, présentés plus haut, sur la traduction respectivement en notations
préfixée et post-fixée. Dans les deux exemples, la chaine obtenue en sortie
représentant la traduction du non-terminal en partie gauche de chaque règle de
production correspond à la concaténation des traductions des non-terminaux en
partie droite avec éventuellement des chaines additionnelles venues se placer entre
ces traductions.
Dans la définition dirigée par la syntaxe associée à la production T ~ T * F, à
savoir, T := T.t Il F.t li '* 1 , la chaine additionnelle 1*1 , comme on peut le voir, est
placée de telle sorte à ce qu'on ait une traduction en notation post-fixée.

On reconsidère la définition dirigée par la syntaxe où toute expression infixée


est traduite en sa correspondante post-fixée formulée comme dans le
Tableau XVII.

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.

Productions Règles sémantiques


E--+ E1 + T E.t := E1.t Il T.t li 1+ 1
E--+T E.t := T.t
T--+ Ti *F T.t := Ti.t Il F.T li 1* 1
T--+F T.t := F.t
F--+ a F.t := 1a 1
F--+ (E) F.t := E.t

Tableau XVII- Définition dirigée par la syntaxe pour la traduction des


expressions arithmétiques en leurs correspondantes post-fixées

Productions Actions sémantiques


E--+E+T {Imprimer('+')}
E--+T
T--+ T *F {Imprimer ('*')}
T--+F
F--+ a Imprimer {('a')}
F --+ (E)

Tableau XVIII- Schéma de traduction dirigée par la syntaxe pour la


traduction des expressions arithmétiques en leurs correspondantes post-fixées

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

Figure 82 : Application des règles sémantiques traduisant l'expression


11 a * (a+ a) 11 en 11 a a a+ * 11
Introduction à la compilation 155

De manière générale la traduction s'effectue parallèlement à l'analyse


syntaxique. De ce fait, l'implantation d'un schéma de traduction aussi simple que
celui décrit ci-dessus ne nécessite pas de construire l'arbre de dérivation.

E
1

~ ~ J {imprimer('*')}

T * F

F
/ (
/\~
E __ )
/ \ ----1~-------
E + T {imprimer ('+')}
a {imprimer ('a')}
T/ \
/ F
/ F ',,,
a {imprimer ('a')}
a {imprimer ('a')}

Figure 83 : Application des actions sémantiques traduisant l'expression


"a *(a+ a)" en "a a a+ *"

Pour confirmation, il suffit de parcourir l'arbre en profondeur et de récolter au


passage tous les résultats des actions {imprimer (' ')}. On a donc ce qui suit :
{imprimer ('a')} ; {imprimer ('a')} ; {imprimer ('a')} ; {imprimer ('+')} ;
{imprimer ('*')}. Cette séquence produit l'expression "a a a + *" qui coïncide
avec le résultat obtenu avec la définition dirigée par la syntaxe.
Chapitre 5
Analyse lexicale

L'analyse lexicale est la première phase du processus de compilation. Son


objectif principal est d'alléger le travail des prochaines phases, en particulier,
celui de l'analyse syntaxique. La construction d'analyseurs lexicaux, repose
toujours sur le même principe, quel que soit le compilateur à réaliser.
Cependant, la stratégie adoptée peut varier d'un compilateur à l'autre. Le
but de ce chapitre est d'abord de définir en quoi consiste l'analyse lexicale,
quel est son rôle, sa finalité, etc. ; ensuite de décrire les outils et procédés
adéquats requis pour sa réalisation.

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

transmettre son type (unité lexicale ou token) représentant une constante


complexe aux prochaines phases du compilateur.
Evidemment, dans ce dernier cas, l'analyse lexicale est plus compliquée. Par
conséquent, tel que se présente le problème, le choix d'une stratégie repose sur la
nature du langage à traiter, mais aussi sur les exigences et difficultés imposées par
le compilateur à réaliser. La frontière entre l'analyse lexicale et l'analyse
syntaxique n'est pas forcément toujours la même. D'ailleurs, il existe des cas où
l'on peut concevoir des compilateurs dans lesquels la syntaxe est définie à partir
des caractères individuels. Mais, les analyseurs syntaxiques qu'il faut alors écrire
sont bien plus complexes que ceux qu'on aurait obtenus en utilisant des
analyseurs lexicaux pour reconnaitre les mots du langage. De nombreuses raisons
justifient ainsi un découpage de la partie analyse d'un compilateur en analyse
lexicale et analyse syntaxique.
La simplification de l'une ou l'autre de ces deux phases
Un analyseur syntaxique qui passe du temps sur des tâches comme l'élimination
des espaces blancs et des commentaires, serait nettement plus complexe que celui
qui les considère comme déjà éliminés au cours de l'analyse lexicale. En outre, les
espaces blancs, les fins de lignes, les commentaires sont grammaticalement
impertinents (ce n'est pas du tout pratique de les inclure dans une grammaire).
De même, comme mentionné ci-dessus, un analyseur syntaxique qui considère
(<réel>, <réel>) comme un modèle qui décrit les nombres complexes, serait,
évidemment, bien plus complexe que celui qui lèguerait cette tâche à l'analyseur
lexical.

L'amélioration de l'efficacité du compilateur


Une part importante du temps de compilation est dépensée dans la lecture du
programme source et son découpage en unités lexicales. L'utilisation de techniques
spécialisées de gestion des buffers (tampons), au cours de la lecture des caractères
d'entrée et du traitement des unités lexicales, permet d'améliorer
substantiellement l'efficacité du compilateur.

L'augmentation de la portabilité du compilateur.


Les particularités de certains symboles d'entrée non standards, comme « Î » en
Pascal, peuvent être confiées à l'analyseur lexical. Cela libère l'analyseur
syntaxique de cette tâche encombrante et augmente, de fait, la portabilité du
compilateur.
A l'issue de la phase de l'analyse lexicale, les flots de caractères formant le
programme source seront groupés en une suite d'unités lexicales. L'analyseur
lexical peut ainsi cacher la représentation physique (lexèmes) des unités lexicales à
l'analyseur syntaxique. Par conséquent, les caractères formant l'instruction
d'affectation : pos := init + vit * 60 seraient renvoyés sous forme d'unités
lexicales à l'analyseur syntaxique comme suit :
<ldf>;
<Assign> ;
<ldf>;
<+>;
158 Chapitre 5

<ldf>;
<*>;
<nbr>.

Les blancs séparant les différents lexèmes de l'instruction


pos := init + vit * 60 seraient, en principe, éliminés au cours de l'analyse
lexicale. Dans la liste précédente, ce sont évidemment les unités lexicales
(représentées par leurs codes respectifs), qui sont renvoyées à l'analyseur
syntaxique. Par contre, les lexèmes (pos, init, vit et 60), sont rangés dans la
table des symboles (s'ils n'y sont pas déjà).
On définira, en section 9 du présent chapitre, en quoi consiste la table des
symboles et quelle est sa finalité.

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>.

Il existe différentes approches pour réaliser un tel travail qui n'est


généralement pas très compliqué, mais qui pose toutefois quelques petits
problèmes. En effet, par exemple, si l'on considère les deux instructions du
langage Fortran, suivantes distinctes, mais toutes les deux valables
(syntaxiquement et sémantiquement correctes) :
DO 10 1=1.15
DO 10 1=1,15
Dans la première instruction, la chaîne DO 10 1 représente l'identificateur (mot)
DOlOI, (les espaces blancs sont ignorés ou supprimés par le préprocesseur du
compilateur Fortran) ; le lexème 1.15 est une constante numérique de type réel.
Au niveau de la deuxième instruction, la chaîne DO est le mot-clé (réservé) qui
exprime un ordre en langage Fortran ; 10 est une constante entière représentant
une étiquette (adresse) ; enfin, 1 et 15 sont des constantes entières représentant
respectivement les bornes inférieure et supérieure de la boucle DO. L'analyseur
lexical ne saura (dans le premier cas) qu'il s'agit de l'identificateur DOlOI qu'une
fois qu'il a rencontré le point "." dans la chaîne "l.15". De même, il ne pourra
déterminer (dans le second cas), qu'il s'agit du mot-clé DO qu'une fois qu'il a
rencontré la virgule "," au niveau de la chaîne "1,15".
Donc, le processus de reconnaissance nécessite la lecture de plusieurs caractères
supplémentaires pour maitriser définitivement une entité lexicale. Mais, cela sous-
entend aussi qu'on a perdu la position qui suit immédiatement la fin du lexème
reconnu. Pour y remédier et contrôler le processus de reconnaissance, on peut
utiliser une technique spécialisée dans la gestion des tampons, au cours de la
lecture des caractères du flot d'entrée et du traitement des unités lexicales. C'est
l'objet de la section 5 du présent chapitre.
Analyse lexicale 159

2 Différents modes de travail d'un analyseur lexical


Dans la littérature informatique on parle souvent de compilateur multi-passe
( multi-pass compiler) et compilateur simple-passe (one-pass compiler).
Une phase est une partie du compilateur qui exprime un traitement bien précis
à travers lequel doit passer chaque instruction d'un programme à compiler. Par
exemple, l'analyse lexicale est une phase, l'analyse syntaxique également ; et toute
combinaison dans le temps de ces phases constitue une passe.
Schématiquement, on peut avoir la combinaison de la Figure 84 qui est une
démarche d'un compilateur multi-passe, où chaque passe contient une phase .

Programme ~ .+ Prg~l
source ...,,,,.- ..
_,,'
,.-
\
~

Prg 01

Figure 84 : Schéma simplifiée d'un compilateur multi-passe

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

Figure 85 : Interaction entre un analyseur lexical et un analyseur


syntaxique

Ainsi, il existe plusieurs manières de solliciter les services d'un analyseur


lexical. Il peut travailler comme un module séparé qui génère en sortie un flux de
tokens, image codée du programme source. Ce flux constituera une entrée pour
l'analyse syntaxique. Il peut également travailler comme un sous-programme qui
fournit juste l'entité {unité lexicale ou token) réclamée par l'analyse syntaxique.
160 Chapitre 5

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.

3 Unités lexicales, modèles et lexèmes


L'analyse lexicale a pour tâche de reconnaitre les lexèmes sur la base de modèles,
et de produire en sortie les unités lexicales associées :
Un lexème correspond à un mot ou une chaîne de caractères apparaissant dans
le flot d'entrée (programme source) de l'analyse lexicale. Par exemple, les
mots: begin, end, (, ), +, :=, X, Xl, Y ... 100, 10.50, etc., sont des
lexèmes pouvant apparaitre dans un programme source.

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.

lexèmes code valeur d 'attribut


X <idf> entrée dans la table des symboles associée à X
.- <Assign> -
y <idf> entrée dans la table des symboles associée à Y
î <exp> -
10 <nbr> valeur 10

Tableau XIX- Lexèmes avec les unités lexicales associées

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

Tableau XX- Exemples de lexèmes, unités lexicales et modèles informels


associés

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.

Définition 4.1 (Mots-clés)


Un mot clé est une instruction spécifique qui fait partie intégrante de la
syntaxe d'un langage. Les mots-clés représentent des lexèmes particuliers
propres au langage considéré. Chaque mot-clé a une signification précise et
joue un certain rôle dans un programme considéré.
Par exemple, dans le langage Pascal les mots-clés begin et end remplissent la
fonction de délimiteurs de blocs, c'est-à-dire marquant le début et la fin d'un
programme, d'une procédure, d'une fonction ou d'une séquence d'instructions
formant un bloc, etc. De même, les mots true et false sont également des mots-
clés représentant des constantes booléennes de valeur 1 et 0 respectivement. Le
mot-clé var est un mot qui annonce la déclaration de variables en langage Pascal.
De même, le mot type, comme son nom l'indique, permet de définir un type en
Pascal.
Dans la majorité des langages de programmation, les mots-clés sont réservés,
et il est donc interdit de les utiliser comme nom de variable, de procédure, de
fonction, etc. Les principaux mots-clés réservés du langage Pascal sont : begin,
end, if, then, else, for, to, do, while, repeat, until, and, or, not, const,
type, var, array, write, read, string, etc.

Définition 4.2 (Identificateurs)


Les identificateurs, appelés aussi mots du programmeur, représentent les
mots choisis par un utilisateur pour nommer ou désigner ses variables,
constantes, fonctions, procédures, etc.
Par exemple, dans la déclaration Pascal var x, y2, v : real ; les mots x, y2, v,
sont des identificateurs (de type réel, indiqué par le mot-clé real). De même, dans
la déclaration de la fonction Function Test (X : real) : boolean ; le mot Test est
un identificateur représentant le nom d'une fonction. Dans la déclaration const
INT = 100 ; le mot INT est un identificateur représentant une constante de
valeur 100. Le Tableau XXI présente quelques exemples d'identificateurs, en
mettant l'accent sur les erreurs à éviter pour obtenir un identificateur correct.
Le symbole souligné 11 11 est acceptable, mais il est déconseillé de l'utiliser
dans les identificateurs. Son utilisation est réservée généralement aux
identificateurs de constantes et à la programmation système (programmation en
mode noyau).
Tous les identifiants sont sensibles à la casse, c'est-à-dire que les minuscules et
les majuscules sont interprétées différemment.
Analyse lexicale 163

Définition 4.3 (Constantes)


On distingue plusieurs types de constantes :
les constantes numériques ;
les constantes booléennes ;
les constantes littérales ou chaînes de caractères ;
Autres : comme les constantes de type date (certains langages actuels,
comme Java, considèrent que date est un type prédéfini comme integer,
real, etc.).

identificateur correct identificateur incorrect raison


formule 1 formule 1 espace blanc non autorisé
variable 1 variable-! le tiret n'est pas autorisé
carbone14 21siecle commence par un chiffre
debut début les accents non autorisés
en deca en deça la cédille n'est pas autorisée

Tableau XXI- Exemples d'identificateurs


Remarque 4.1
Il existe deux façons d'exprimer les constantes :
de manière littérale (constantes littérales) comme 5, +65, 11 maison 11 , "jardin",
etc. ;
de manière indirecte (constantes non littérales), c'est-à-dire la constante est
exprimée par une variable déclarée (un identificateur). En Pascal, on déclare
const chaine = 'yoyo' ; c'est-à-dire que chaine est l'identificateur de la
constante de valeur yoyo.
Les constantes numériques, elles aussi, peuvent être subdivisées en sous-
classes :
• constantes entières comme 5, 10, +65, -32, etc. Là aussi, dans certains
langages actuels comme C, on parle d'entiers longs, d'entiers courts, etc.
• constantes réelles simples sans exposants comme, 5.45, -10.33, 0.55,
+40.38, etc.
• constantes réelles avec exposant comme 5.567E+4, 12E+6, +23.76E-4, etc.
Les constantes booléennes sont représentées, dans la plupart des langages, par
les mots-clés réservés true et false.
Les constantes chaines de caractères sont formées de suites de caractères,
comme la constante chaine littéral "maison", ou la constante nommée
"logement" ayant pour valeur 'villa', définie par la déclaration const logement
= 'villa'.
Remarque 4.2
Comme mentionné, ci-dessus, une constante peut être littérale, c'est-à-dire
exprimée par sa valeur ou bien déclarée et désignée par un nom (identifiant)
permettant de l'identifier à chaque utilisation dans le programme. Un des
164 Chapitre 5

avantages de cette façon d'exprimer une constante provient de considérations


pratiques. D'une part, quand une constante est déclarée par son nom, cela permet
de ne pas avoir à la redéfinir à chaque utilisation dans le programme. En effet, par
exemple, une constante numérique comme -124.5674329087E+2, si elle est
déclarée par const val = -124.5674329087E+2, cela suppose qu'elle est définie
une fois pour toute dans le programme par le mot « val » ; il suffit donc d'utiliser
ce dernier, sans avoir à réécrire, à chaque fois, la valeur -124.5674329087E+2.
D'autre part, vu la longueur du lexème -124.5674329087E+2, il serait judicieux
que l'analyse lexicale n'ait pas à le re-scanner (traiter) une nouvelle fois. En
conséquence, cela évite également que, le compilateur ait à recalculer, à chaque
fois, la valeur associée.
En langage C, les constantes sont définies grâce à la directive #define du
préprocesseur qui permet de remplacer toutes les occurrences du mot qui suit
#define par la valeur immédiatement derrière. Par exemple, la directive #define
val -124.5674329087E+2 remplacera tous les identifiants «val» (sans
guillemets) par la valeur -124.5674329087E+2, sauf dans les chaînes de caractères.
Toutefois, avec cette méthode, les constantes ne sont pas typées ; il faut donc
utiliser la directive #define avec parcimonie. Il est ainsi préférable d'utiliser le
mot-clé const, qui permet de déclarer des constantes typées comme const int
vingt = 20. De plus, cela permet d'éviter certains problèmes du #define, qui
consiste à faire du « chercher-remplacer » textuel sans réfléchir.

Définition 4.4 (Opérateurs)


La classe des opérateurs représente tous les mots ou symboles spéciaux qui
participent à la construction de toute sorte d'expression.
Par exemple, les symboles spéciaux et mots-clés réservés >, *, +, and, or,
dans l'expression logique a > b * (c + 5) and x or y, sont des opérateurs. Il
faut noter que certains opérateurs comme and et or représentant le ET et le OU
logiques sont également des mots-clés.
On distingue les opérateurs arithmétiques et les opérateurs logiques :
Dans la catégorie des opérateurs arithmétiques, il y a des opérateurs binaires
ou dyadiques comme+,-, *, /,c'est-à-dire nécessitant deux opérandes. Il y a
également des opérateurs unaires comme le « moins » et le « plus » unaires ou
monadiques (-et+), qui s'appliquent à un seul opérande. Comme on peut le
constater, ces derniers ont la même syntaxe que leurs homologues dyadiques.
Cette question doit impérativement être résolue au niveau lexical. Autrement
dit, c'est à l'analyse lexicale de déterminer le type (unité lexicale) exact du
lexème reconnu ( + ou - unaire ou bien + ou - binaire), et le renvoyer à
l'analyse syntaxique. Il existe également d'autres opérateurs arithmétiques
(binaires et unaires) spécifiques représentés par des mots-clés réservés comme
div, mod et round. Le mot-clé div (binaire) par exemple, représente la
division entière en Pascal. Le mot-clé mod, est un opérateur-fonction (binaire)
qui délivre le reste de la division entière entre deux entiers. Le mot round est
un opérateur unaire qui transforme un nombre réel en l'entier immédiatement
Analyse lexicale 165

supeneur ou inférieur correspondant ; 25.65 et 25.43 donneraient


respectivement 26 et 25.

Dans la classe des opérateurs logiques, on distingue (selon le langage) des


opérateurs à une position et des opérateurs à deux positions. Les symboles <,
=, >, -:!-, -,, etc., sont des opérateurs à une seule position. Les symboles
doubles <>, <=, >=, etc., sont des opérateurs à deux positions. Au milieu
de ces opérateurs on trouve les opérateurs relationnels <, =, >, -:!-, <>, <=,
>=, etc., qui établissent une relation entre deux opérandes arithmétiques ; le
résultat est évidemment logique (booléen). On rencontre également des
opérateurs purement logiques représentés par des mots-clés comme and, or et
net qui établissent une relation entre des opérandes booléens. Par exemple,
l'expression net x and y or z est une expression logique formée uniquement
d'opérateurs booléens et d'opérandes logiques (ayant pour valeur true ou
false). En revanche, a > b * (c + 5) and x or y, est une expression logique
construite à base d'opérateurs et d'opérandes, mixtes (arithmétiques,
relationnels et booléens).

Définition 4.5 (Séparateurs ou délimiteurs)


Les séparateurs sont des mots ou des symboles de ponctuation, permettant de
séparer deux entités consécutives. On sous-entend, ici, par entité : un
lexème, une expression, une instruction, un bloc d'instructions, un
commentaire, etc.
Par exemple :
{ et } deux accolades, permettent de délimiter un bloc d'instructions en
langage C et servent également à délimiter un commentaire en langage Pascal ;

/* et */ sont des délimiteurs doubles de commentaires en langage C ;

« ; » le point-virgule représente un séparateur d'instructions ou de blocs


d'instructions en Pascal ou en C ;

(* et *) sont des délimiteurs doubles de commentaires en Pascal ;

( et ) sont des séparateurs permettant d'isoler des sous-expressions comme


(a+ b) et (c + d) dans une expression comme (a+ b) * (c + d). Ces
parenthèses jouent en quelque sorte le rôle d'opérateurs dans des expressions.

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;

« := » est le symbole de l'affectation en Pascal ;

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.

4.1 Codification des lexèmes


Comme mentionné précédemment, chaque lexème rencontré, au niveau de
l'analyse lexicale, sera communiqué à l'analyse syntaxique sous une forme codée,
nommée unité lexicale. Cette dernière consiste en une unité atomique représentée
généralement par un couple (code, valeur d'attribut). Le premier composant,
c'est-à-dire code, représente le type ou le code du lexème reconnu, qui peut
être un identificateur, un mot-clé, une constante, un opérateur, séparateur, etc.
Quant au deuxième composant noté valeur d'attribut, qui n'est pas toujours
présent dans le couple (certaines entités n'en nécessitent pas), consiste, soit en un
pointeur vers la table des symboles, où l'on range effectivement le lexème
correspondant (cas d'un identificateur ou d'une constante), soit en une valeur
d'attribut associée au lexème (la valeur effective d'une constante par exemple).

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

Figure 86: Un buffer d'entrée en deux moitiés

Le buffer est divisé en deux parties égales de N caractères chacune. N étant la


taille d'un bloc disque qui est par exemple = 1024, ... ou 4096 selon les
caractéristiques du système utilisé.
Le caractère spécial # ne peut faire partie d'un programme source, il indique
seulement:
la fin du buffer ;
la fin de la moitié gauche ;
la fin du texte d'entrée.
Le symbole # peut, par exemple, être eof (end of file ou fin de fichier) qui
doit être différent de tout autre caractère du programme source.
Cette technique joue un rôle double :
168 Chapitre 5

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.

Définition 6.1 (Définitions régulières)


Soit Vr un alphabet de base (terminal}. On appelle définition régulière, une
suite de règles définie comme suit :
di~ ri
d2~ r2
170 Chapitre 5

di est un nom distinct et ri est une expression régulière sur l'ensemble


VTu{di, d 2 , ••• , di-Ù, c'est-à-dire les symboles de base de VT et les dk {k = 1, i -1)
définis jusqu'alors.
En pratique, il est toujours commode d'utiliser des symboles mnémoniques
pour distinguer les noms des symboles d 1 , d 2 , ... , dn. On peut par ailleurs les écrire
en caractères gras pour les mettre en valeur.
Par exemple, la définition régulière des identificateurs comme ceux du langage
Pascal est donnée par les règles de production suivantes :
lettre ~ a 1 b 1 .. . 1 A 1 B ... 1 Z
digit ~ 0 1 1 ,... 1 9
idf ~ lettre (lettre J digit)*

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

On peut également utiliser certaines abréviations pour simplifier la notation


des définitions régulières.

6.1 Notations abrégées


Fermetures transitives (positive : r + et positive-réflexive : r *) :
r* = r+ le
r + =rr =rr *
L'opération unaire « ? » (point d'interrogation), signifie 0 ou 1 fois, c'est-à-dire
qu'on a : r ? = r 1 e

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.

Définition 6.2 (Expressions régulières modifiées)


On définit de manière récursive les expressions régulières modifiées et les
ensembles qu'elles dénotent.
Si R est une expression régulière simple, alors elle est une expression régulière
modifiée, et elle désigne l'ensemble R ;

Si R est une expression régulière modifiée alors :


• R+ désigne l'ensemble RR* ;
• R*n désigne l'ensemble {e}u Ru R 2 u ... u Rn;
• R+n désigne l'ensemble R u R 2 u ... u Rn;

Si Ri et R2 sont des expressions régulières modifiées alors :


• Ri - R 2 est une expression régulière modifiée qui désigne l'ensemble
{x 1 x E Ri et x <t: R2} ;
• Ri n R 2 est une expression régulière modifiée qui désigne l'ensemble
{x 1 x E Ri et x E R2} ;
• Ri 1 R 2 est une expression régulière modifiée qui désigne l'ensemble
{x 1 XE RiouxE R2}

Par exemple, les identificateurs en Fortran peuvent être décrits par la


définition régulière suivante :

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

7 Reconnaissance des entités lexicales


A l'issue des étapes de choix des lexèmes, le problème de la reconnaissance des
entités lexicales apparaît comme une étape où l'on raisonne en
termes d'algorithmes d'analyse du texte source et du découpage de celui-ci en un
ensemble de lexèmes qui sont renvoyés sous forme codée (unités lexicales) à
l'analyse syntaxique.

Ces algorithmes constituent une partie très importante du processus de


l'analyse lexicale. Cette partie, qu'on appelle parfois «scanner », consiste en :
la lecture du flot d'entrée (texte du programme source) ;
l'élimination des caractères superflus (espaces blancs, commentaires,
tabulation, etc.) ;
la reconnaissance des lexèmes ;
l'association du token approprié à chaque lexème reconnu ;
l'envoi du token identifié sous une forme adéquate à l'analyseur syntaxique ;
d'autres tâches secondaires non moins intéressantes comme la détection des
erreurs lexicales (identificateur trop long, caractère illégal...), l'incrémentation
du compteur de ligne à la rencontre du caractère fin de ligne, etc.

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 ;

Simulation du comportement de l'automate à l'aide de la table de transition


associée.

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

7 .1 Analyse lexicale indirecte


L'analyse lexicale indirecte peut être appelée pour analyser une entité d'un type
spécifié. Après avoir lu une partie de la chaine d'entrée, l'analyseur lexical vérifie
si un certain lexème d'un type spécifié est apparu. Si l'ensemble des chaines (le
modèle) correspondant à ce lexème est désigné par une expression régulière, le
problème de la construction d'un analyseur lexical indirect apparait comme le
problème de réalisation d'un transducteur d'états finis qui fonctionne comme
suit:
lit la chaine d'entrée caractère par caractère sans avoir à délivrer une
quelconque chaine en sortie, jusqu'à l'apparition d'un lexème d'un type
spécifié (le transducteur a atteint un état final) ;

signale qu'un lexème d'un type spécifié est effectivement apparu ;

délivre en sortie la chaine de caractères qui constitue le lexème reconnu.


L'état final du transducteur est en soi une indication permettant d'affirmer
qu'un certain lexème est apparu. Cependant, l'analyseur lexical doit lire un ou
plusieurs caractères supplémentaires pour s'assurer qu'il s'agit bien d'un lexème
du type spécifié. Par ailleurs, l'analyse lexicale indirecte nécessite parfois le retour
au lexème déjà reconnu, car ce dernier peut être rejeté par l'analyse syntaxique.
On optant pour un analyseur lexical indirect, on doit être prudent pour ne pas
effectuer d'opération erronée sur la table des symboles. Normalement, on ne doit
insérer un identificateur dans la table des symboles que si on est sûr qu'il est
valide. Alternativement, on peut développer un mécanisme permettant de
supprimer l'entrée à la table des symboles s'il s'avère que l'identificateur inséré
n'est pas valide.
Le problème de l'analyse lexicale indirecte consiste donc essentiellement en la
construction d'un automate d'états finis déterministe, à partir d'une expression
régulière, et son implémentation sur machine.

7.2 Analyse lexicale directe


L'analyse lexicale directe peut être appelée pour reconnaitre un lexème et
renvoyer son type (unité lexicale), en réponse, à l'analyse syntaxique. Dans ce cas,
un moyen efficace consiste à construire des automates d'états finis et à simuler
leur fonctionnement en parallèle à l'aide d'un transducteur d'états finis (un
transducteur est un automate muni d'un ruban de sortie). Si ces automates sont
non déterministes et leurs états disjoints, il faut les fusionner en un seul automate
qu'il convient de convertir ensuite en automate déterministe. L'automate résultant
peut être considéré comme une sorte de transducteur. Ainsi, quand ce dernier
entre dans un état contenant un état final de l'un de ses automates composants, il
doit s'arrêter et délivrer en sortie le token (unité lexicale) approprié. Cependant,
en pratique les choses ne sont pas toujours aussi simples. Par exemple, si un
identificateur peut être n'importe quelle chaine de caractères, sauf un mot-clé, il
n'est pas commode en pratique de le définir par une expression régulière exacte,
car l'automate d'états finis correspondant peut être compliqué et requérir de
nombreux états. Au lieu de cela, on peut utiliser une simple définition pour les
174 Chapitre 5

identificateurs et laisser le soin à l'automate associé de prendre la bonne décision.


Ce dernier doit entrer dans un état qui comprendrait un état final de l'automate
d'un certain mot-clé et un état de l'automate des identificateurs, avec comme
prochain symbole un espace blanc ou un signe spécial indiquant la fin du lexème
rencontré. Ainsi, après avoir reconnu un lexème, l'automate doit être à l'état final
d'acceptation, et sa décision pourrait, par exemple, être fondée sur la stratégie qui
considère qu'un mot-clé est prioritaire par rapport à un simple identificateur.
On considère un exemple un peu abstrait où l'on suppose qu'un identificateur
consiste en n'importe quelle chaine composée des caractères D, F, I et 0, suivie
d'un espace blanc, excepté les mots-clés DO et IF qui ne nécessitent pas d'être
suivis par un espace blanc, mais ne doivent pas être suivis, non plus,
immédiatement par un des caractères D, F, I et 0 [Aho, 73].
Les identificateurs sont reconnus par l'automate fini dont le diagramme de
transition est celui de la Figure 87 (a), le mot-clé DO est reconnu par celui de la
Figure 87 (b), et enfin, l'autre mot-clé noté IF est reconnu par le diagramme de
la Figure 87 (c). Tous ces automates sont déterministes, quoique cela ne soit pas
indispensable, en général. La fusion des trois automates est illustrée par le
diagramme de la Figure 88.

D, F, I, 0

(a) Identificateurs (b) DO

(c) IF

Figure 87: Automates pour l'analyse lexicale

Le symbole :=>représente le caractère blanc. L'état 2 de l'automate résultant de


cette fusion est un état final indiquant qu'un identificateur a été reconnu.
Cependant, les états <1,8> et <1,5> sont ambigus. Ils pourraient indiquer IF ou
DO respectivement, ou bien tout simplement une portion d'un identificateur
comme DOFF ou IFFD, etc. Pour résoudre ce conflit, l'analyseur doit examiner
(lire) un symbole supplémentaire. Si ce dernier correspond au caractère D, 0, I ou
F, on a alors le préfixe d'un identificateur. Dans le cas contraire, ça doit être un
autre caractère, y compris le caractère blanc (on suppose qu'il y a plus de
caractères que les cinq mentionnés). L'automate transite alors vers un nouvel état
(9 ou 10), et émet un signal, indiquant qu'un DO ou un IF a été détecté avant la
lecture du symbole supplémentaire. Si l'automate transite à l'état 2, il émet un
signal indiquant qu'un identificateur a été rencontré avant la lecture du dernier
caractère (le caractère supplémentaire lu).
Analyse lexicale 175

Puisque c'est la sortie (lexème représentant un identificateur ou le mot-clé DO


ou IF) du dispositif (automate résultant) et non pas l'état qui est important, alors
les états 2, 9 et 10 peuvent être identifiés et n'auront, en fait, aucune
représentation dans l'implémentation (simulation par programme de l'automate).
* D. F. 1. 0

* D. F, 1. 0
Figure 88 : Automate résultant pour l'analyse lexicale

7.3 Simulation de l'automate par le diagramme de transition associé


Etant donné un ensemble de diagrammes de transition représentant
respectivement des automates d'états finis. On transformera chaque diagramme en
une séquence d'instructions ayant pour finalité de renvoyer l'unité lexicale
associée à tout lexème reconnu par le diagramme. Par exemple, soit à reconnaitre
les entités lexicales qui consistent en : les mots-clés : for, while ; les
identificateurs ; les constants entières ; et enfin, les opérateurs : +, -, *, /
Les automates d'état finis qui acceptent ces entités sont représentés
respectivement par les diagrammes de transition des Figures 89, 90, 91 et 92.
0 r é LD *
<for>
Q
=>
éLD
*
w
e <while>
h

Figure 89 : Automate fini pour l'analyse lexicale des mots-clés for et while
176 Chapitre 5

eLD *
L
<idf, adr>

Figure 90 : Automate fini pour l'analyse lexicale des identificateurs

eD
*
D
<const, val>

Figure 91 : Automate fini pour l'analyse lexicale des constantes entières

=>

Figure 92: Automate fini pour l'analyse lexicale des opérateurs{+,-,*,/}

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

états 3, 9, 12 et 15 appartenant respectivement aux diagrammes de transition


précédents, sont considérés comme des états finals, mais dans le contexte de
l'analyse lexicale, d'autres considérations peuvent entrer en jeu. En effet, par
exemple à l'état 3 (théoriquement final), il faut rencontrer un caractère qui n'est
ni une lettre ni un chiffre (donc li!: LD), pour transiter effectivement vers l'état
final artificiel noté 4 (état final simple), et délivrer en sortie le token associé, noté
<for> sur la dernière transition du diagramme. Il en est de même pour les autres
états : 9, 12 et 15. Les états finals 18, 19, 20 et 21, en revanche, ne sont pas
repérés par un aster « * » ; donc, ne nécessitent pas que soit retiré le dernier
caractère lu.
On donne ci-après un aperçu général sur les séquences d'instructions associées
respectivement aux diagrammes de transition présentés ci-dessus par les Figures
89, 90, 91 et 92. On s'arrange de sorte qu'à chaque état, à l'exception des états
finals, il y ait la lecture d'un caractère qui sera comparé au caractère qui force la
transition à partir de cet état vers un état successeur. Mais, afin qu'il y ait une
certaine cohérence dans l'écriture et la structuration du simulateur, chaque
numéro d'état joue le rôle d'une étiquette qui repère la première instruction de la
séquence d'instructions associée à l'état en question. Il faut noter cependant que
les états finals ici sont des repères indiquant uniquement qu'un certain lexème a
été reconnu. Autrement dit, il n'y aura pas de nouvelle lecture à partir d'un état
final d'acceptation (état final simple, d'où ne sort aucun arc).
0 : readcar (c) ;
si c E {'f', 'w'} alors
cas de
"f " : aller à 1
"w" : aller à 5
sinon aller à 11 ;
1 : readcar (c) ;
si c = 'o' alors aller à 2
sinon début
retract (c) ;
aller à 11
fin

17 : readcar (c) ;
cas de
"+" : traiter l'opérateur +
"-" : traiter l'opérateur -

La procédure readcar (c) fournit le caractère courant du flot d'entrée, tandis


que la procédure retract (c) fait reculer le pointeur lookahead là où il était à l'état
initial du diagramme courant et repart avec le diagramme de transition suivant.
Pour reconnaitre, par exemple, l'identificateur "whi" le simulateur doit transiter
d'abord par les étiquettes 0, 5, 6, 7 et 8. Cependant l'état 8, n'est pas final, alors
que le mot "whi" est terminé. Donc, il y a comme une espèce de blocage à l'état 8
178 Chapitre 5

où le dernier caractère lu est-:;:. "e". Pour reprendre l'analyse, le simulateur doit se


brancher à l'étiquette 11 qui représente l'état initial d'un nouvel automate, c'est-
à-dire celui qui reconnait les identificateurs.
En somme, une suite de diagrammes de transition peut être convertie en un
simulateur qui recherche des unités lexicales spécifiées par les diagrammes.
Chaque état donne lieu à une séquence ou segment de code. On utilise la
procédure readcar () pour lire le caractère suivant dans le tampon d'entrée,
avancer le pointeur lookahead à chaque appel et retourner enfin le caractère
rencontré. Si ce dernier ne coïncide pas avec celui de la transition au niveau de
l'état courant, une procédure retract () se charge de faire reculer le pointeur
lookahead à la position début-de-lexème afin de démarrer la recherche d'une
nouvelle unité lexicale spécifiée par le prochain diagramme de transition.
Evidemment, si tous les diagrammes de transition ont été essayés sans succès, une
procédure appropriée de traitement des erreurs sera appelée au niveau de la
procédure retract ().
La taille du programme simulateur est proportionnelle au nombre d'états et
d'arcs dans les diagrammes de transition. Dans le cas d'un langage de
programmation typique, le nombre d'états est de plusieurs centaines,
particulièrement à cause du nombre de mots-clés. Mais, étant donné que les mots-
clés représentent une sous-classe particulière de la classe des identificateurs, une
technique simple et très payante consiste à les placer de manière appropriée dans
la table des symboles avant de démarrer l'analyse lexicale. Donc, le nombre
d'états et d'arcs va considérablement diminuer ; ce qui fera ainsi décroitre
substantiellement la complexité du simulateur. Ainsi, un mot-clé sera d'abord
traité comme n'importe quel identificateur conformément au diagramme de
transition des identificateurs. Il sera ensuite comparé aux entités logées dans la
table des symboles pour vérifier s'il correspond ou non à un mot-clé.

7.4 Simulation de l'automate par la table de transition associée


Une fois la table de transition en mémoire, l'analyseur lexical la parcourt jusqu'à
isoler un lexème ou détecter une erreur lexicale. La simulation peut avoir lieu sur
un automate fini déterministe ou non déterministe.
On décrit ci-après les deux cas de figure :
Cas d'un automate fini déterministe
readcar (c) ; state f-- so ;
tant que car -:;:. eof faire
début
state f-- 1 (state, c) ; readcar (c)
fin
fait ;
si state E F alors
retourner "oui"
sinon retourner "non"
Analyse lexicale 179

Cas d'un automate fini non déterministe


readcar (c) ; state f- e-closure ({s0}) ;
tant que car -=t. eof faire
début
state f- e- clos ure (I (state, c)) ;
readcar (c)
fin
fait j
si state n F -=t. <I> alors
retourner "oui"
retourner "non" ;

On rappelle que 1 est la table qui représente la fonction de transition de


l'automate. La variable « state » représente un état quelconque de l'automate,
tandis so représente l'état initial de l'automate. Le symbole eof, comme expliqué
auparavant, correspond à la fin de fichier, c'est-à-dire un caractère différent de
tout autre caractère des lexèmes reconnus par l'automate. L'ensemble F étant,
bien entendu, l'ensemble des états finals. La fermeture notée e-closure, dans le cas
d'un automate non déterministe, est une fonction qui renvoie l'ensemble des états
de l'automate accessibles à l'aide d'une €-transition.
Il est évident que l'approche qui consiste à simuler l'automate par la table de
transition est plus générale et techniquement très différente de celle qui simule le
fonctionnement de l'automate directement par son diagramme de transition. On a
déjà vu précédemment, dans le cas de la simulation de l'automate par son
diagramme, que la taille du simulateur est proportionnelle au nombre d'états et
d'arcs dans les diagrammes de transition. Mais, on a remarqué le contraire avec la
table de transition, à savoir, que la taille du simulateur est petite et, de surcroit,
indépendante du nombre d'états et d'arcs dans les diagrammes de transition.
Cependant, si la taille du simulateur demeure inchangée, quel que soit le langage
considéré, il n'en est pas de même, en ce qui concerne la taille de la table de
transition, qui elle, peut changer d'un type de langage à l'autre, particulièrement,
lorsqu'on n'initialise pas la table des symboles par les mots-clés qui nécessitent
d'être représentés par plusieurs centaines d'états.
Les différences fondamentales entre les deux méthodes précédentes concernent
essentiellement leurs complexités temporelle et spatiale quand elles emploient les
tables de transition d'automates d'états finis déterministes et non déterministes.
Le Tableau XXII résume les complexités temporelle et spatiale nécessaires pour
déterminer si une chaine d'entrée x appartient au langage dénoté par l'expression
régulière r en utilisant des reconnaisseurs construits à partir d'automates finis
déterministes et non déterministes.
Conformément aux résultats sur les exigences temps/place recueillis dans le
Tableau XXII, on peut implanter la fonction de transition par une table de
transition pour simuler un automate d'états finis déterministe sur la chaine
d'entrée x en un temps proportionnel à la longueur de x indépendamment du
nombre d'états de l'automate. Une fois que l'automate fini est construit, l'analyse
180 Chapitre 5

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

Tableau XXII- Exigences en temps et en place pour reconnaitre des


expressions régulières

Default Base Next Check

1
~

s q -- r t

Figure 93 : Structure de données pour représenter des tables de transition


avec compromis temps-place [Aho, 86]

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

sinon suivant f- suivant (Default [s], a)

La finalité de la structure de la Figure 93 est de raccourcir les tableaux Next


et Check, en tirant le meilleur parti de la similitude entre états. Pour donner une
idée sur la manière d'utiliser cette technique, on considère un automate d'états
finis qui reconnait les identificateurs et le mot-clé if. Le diagramme de cet
automate est décrit dans la Figure 94.
Ll ={lettre} - {i}
L2 = {lettre, digit} - {.f}
LD = {lettre, digit}
Pour rappel, l'état final d'acceptation est marqué par un aster « * » qui signifie
que le dernier caractère lu n'appartient pas à LD (noté E: LD sur la Figure 94), et
doit donc être retiré.
L'état 1 constitue l'état par défaut (Default) pour les états marqués par #
comme par exemple, l'état 2. A partir de ce dernier et sur le caractère f,
l'automate transite vers l'état spécial 3, mais à part ça, l'état 2 se comporte
comme l'état 1, c'est-à-dire 1 (2, x) = 1 (1, x) = 1, pour x:;:. f Un état par défaut
comme 1 peut donc être utilisé à la place des états marqués par #, ce qui permet
de raccourcir les tableaux N ext et Check.
Pour simplifier la confection des quatre tableaux Default, Base, Next et
Check, on admet les valeurs numériques fictives suivantes pour les caractères de
l'alphabet utilisé, à savoir, les lettres alphabétiques minuscules [a - z] et les
chiffres [O - 9].
[a - z]: 0 - 25
[O - 9] : 26 - 35

Figure 94 : Automate fini des identificateurs et du mot-clé if


On obtient donc, conformément à la technique de compression envisagée, un
aperçu des quatre tableaux, illustré par la Figure 95.
Toutes les entrées de l'état 1 (hormis l'état 4 qui représente un état final
d'acceptation), sont stockées dans le tableau Next dans les cases 0 à 35, c'est-
à-dire sur tout l'alphabet [a - z 0 - 9] de l'automate. On peut voir, par
exemple, que sur la lettre a (0), le numéro de la case est 0, et Next [O] =l. De
même, sur le chiffre 9 (35), la case est 35 et Next [35] = 1 (état 1).
182 Chapitre 5

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.

Default Base Next Check


0 1 0 -- 0 1 a 1
1 1 -- 1 1 1 b 1
2 1 2 -- 2 1 1

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

En somme, les deux approches, à savoir, la simulation d'un automate fini


directement par son diagramme de transition et la simulation par la table de
transition associée, présentent des différences essentiellement techniques. En effet,
on a déjà vu que la taille d'un simulateur basé sur les diagrammes de transition
est proportionnelle au nombre d'états et d'arcs dans les diagrammes de transition.
En revanche, la taille du code du simulateur lui-même est indépendante de la
taille de la table de transition. Mais, cette dernière peut s'avérer particulièrement
volumineuse quand les mots-clés sont interceptés par l'analyseur lexical, c'est-à-
dire décrits par des diagrammes de transition, à défaut d'être installés au
préalable dans la table des symboles. Le choix d'une approche dépend
essentiellement des avantages qu'elle offre. Il faut retenir, cependant, que
l'approche de la table de transition est plus générale, ce qui représente un
avantage certain, puisque le même simulateur peut être reconduit, quel que soit le
langage ; il suffit de changer les données de la matrice (ou de la table) de
transition. Cette caractéristique ne se retrouve pas dans l'approche basée sur les
diagrammes de transition. Les diagrammes peuvent changer d'un langage à
l'autre. On ne se soucie donc pas du problème de l'espace puisqu'on n'a pas de
table de transition à gérer, mais, on aura à écrire tout de même, pour chaque
langage, un simulateur ad-hoc adapté aux diagrammes de transition associés.

7.5 Exemples illustratifs avec les diagrammes de transition


On considère une portion de langage avec les tokens suivants :
<begin> pour le lexème begin ;
<end> pour le lexème end ;
<const, val> comme par exemple : 0, -5, 250 ; informellement un lexème de ce
type est un mot formé de digits [O - 9], précédé éventuellement d'un signe
{+, -} ;
<idf, adr> comme par exemple les mots : a, Al, max ; informellement un tel
lexème correspond à un mot formé de lettres alphabétiques et/ou de chiffres,
commençant nécessairement par une lettre alphabétique.
On associe à chaque token le diagramme de transition adéquat correspondant.
Les diagrammes de transition sont utiles précisément dans la spécification des
tokens et la structuration de l'analyseur lexical. On décrit, dans la Figure 96, les
diagrammes de transition associés aux tokens envisagés dans l'échantillon de
langage précédent.

E LD *

<begin>

E LD *

<end>
184 Chapitre 5

D *
e:D

=>
<const, val>

LD
L *
e: LD
=>

<idf, adr>

Figure 96 : Diagrammes de transition respectivement des mots-clés (begin


et end}, des constantes entières et des identificateurs
On rappelle que :
LD = {lettre, digit} ;
D ={digit};
L ={lettre}
On précise également le rôle de certaines variables utilisées dans les procédures
où sont définies les différentes actions du simulateur des diagrammes de transition
de la Figure 96 :
La fonction nexttoken retourne le prochain token. Elle essaie tous les
diagrammes de transition successivement en démarrant avec le diagramme
commençant à l'état O. En cas d'échec sur le diagramme courant, elle reconsidère
le lexème en cours à partir du début, et essaie le prochain diagramme spécifié par
la fonction nommée echec.
Les autres routines sont les suivantes :
erreur: routine d'erreurs ;
getnonblank : retourne un caractère non blanc ;
nextchar : retourne le caractère suivant ;
letter ( c) / digit ( c) : contrôle si le caractère c correspond à une lettre ou à un
chiffre.
Les variables la et da représentent respectivement le lookahead et début-de
lexème;
La variable start contient à tout moment la valeur de l'état initial du
diagramme en cours ;
La variable state contient la valeur de l'état courant.
On donne ici, à titre indicatif, un aperçu sur le parcours des diagrammes de
transitions associés aux entités lexicales de la Figure 96.
function echec ;
begin
la:= dl;
Analyse lexicale 185

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.).

Les mots-clés sont des identificateurs, c'est-à-dire qu'on ne définira pas de


diagrammes de transition particuliers pour les spécifier. Ils doivent néanmoins
apparaitre dans la table des symboles afin qu'ils soient distingués des autres
identificateurs (noms de variables, de fonctions, de procédures, etc.). Ils doivent
donc être reconnus d'abord par le diagramme de transition en tant
qu'identificateurs ; ensuite au niveau de la table des symboles, on leur attribue
leurs codes respectifs pour les distinguer des identificateurs (mots du
programmeur).
186 Chapitre 5

Un certain nombre de questions peuvent également être soulevées en ce qui


concerne le traitement réservé aux constantes, particulièrement, les constantes
numériques, comme les nombres signés ou non, avec ou sans exposant, etc.
Par exemple, quelle est la stratégie à adopter pour la reconnaissance des
constantes spécifiées par la définition régulière donnée ci-après ?
digit ~ 0 1 1 1 ... 1 9
nombre~(+ 1 -) ? digit+(• digit+)? (E (+ 1 -) ? digit+)?
Cette définition est de la forme ( + 1 -) ? digits fraction exposant sachant
qu'elle est équivalente aux règles suivantes :
digit ~ 0 l 1 l ...19
digits~ digit +
fraction ~ (• digits) ?
exposant ~ (E ( + 1 - ) ? digits) ?
nombre ~ ( + 1 -) ? digits fraction exposant
Les termes fraction et exposant sont optionnels dans la définition régulière.
Il faut prendre en considération ces options dans la reconnaissance des entités
concernées. Les diagrammes de transition correspondant à la définition régulière
( + 1 -) ? digit + (• digit +) ? (E ( + 1 -) ? digit +) ? , sont donnés dans la
Figure 97.

D D D

D
'E' D

D D

D
D

D <l: D u{'E',•}

Figure 97 : Diagrammes de transition pour les nombres réels


Analyse lexicale 187

D = [O - 9] est l'ensemble des chiffres décimaux. Le symbole « • » est la


virgule des nombres fractionnaires. Le symbole 'E' représente 10 pour les
puissances de 10.
Pour traiter certains cas d'erreurs dans le flot d'entrée, il est possible d'utiliser
l'information concernant des points du langage non exprimés dans les définitions
régulières des unités lexicales. Par exemple, lorsque le lexème 89E se présente à
l'entrée, le simulateur échoue au niveau de l'état 5, si le prochain symbole
d'entrée n'appartient pas à l'ensemble D u { +, -}. Plutôt que de retourner le
nombre 89, il serait judicieux de signaler l'erreur et faire en sorte que le lexème
correspond à 89EO.
Une stratégie efficace que l'on peut adopter pour la reconnaissance d'une
constante numérique par un des trois diagrammes de la Figure 97 est de faire en
sorte que le lexème, pour une unité lexicale donnée, soit le plus long possible. Par
exemple, quand le lexème qui se présente est 26.89E-2, l'analyseur lexical ne doit
pas s'arrêter après avoir vu 26 ou même 26.89. Partant des états 15, 9 et 0 de la
Figure 97, on atteint les états finals d'acceptation après avoir vu 26, 26.89 et
26.89E-2, respectivement. Les diagrammes de transition dont les états initiaux
sont 15, 9 et 0 correspondent respectivement aux expressions régulières (+ 1-) ?
digits, (+ 1 - ) ? digits fraction et ( + 1 - ) ? digits fraction exposant, et
conformément à la stratégie du lexème le plus long envisagée, il va falloir essayer
les états initiaux dans l'ordre inverse 0, 9 et 15. Ainsi, si on présente le lexème
50.E12, il sera reconnu directement sans retour arrière. En revanche, s'il s'agit du
lexème 50, il y a deux retours arrière : au niveau de l'état 2, ensuite à l'état 11.
Ces retours arrière sont une forme de non déterminisme qui engendre des
comparaisons redondantes dans les diagrammes de transition de la Figure 97. On
peut neutraliser ces retours arrière, et améliorer en conséquence l'analyseur :

Une première approche consiste à combiner les trois diagrammes en un seul


comme celui de la Figure 98.
Une autre approche consiste à modifier la réponse à l'échec au cours du
processus de parcours d'un diagramme. L'exemple abstrait de la Figure 99
permet d'expliquer, à travers certains cas, l'utilité de cette approche.

<entier> <réel simple>


"\.1;-
&&/
ql:>&c
&.t,o
o.~~
t.:::,,
D

*
=>

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.

(a) diagrammes de transition


pour a, abb et a· b+ b

(b) diagrammes de transition b


pour a, abb et a· b+
combinées

Figure 99 : Diagrammes de transition reconnaissant trois modèles


différents [Aho, 86]
Analyse lexicale 189

La simulation du fonctionnement de l'automate fini non déterministe combiné


des expressions régulières a, abb et a*b+ sur la chaine aaba montre que l'ensemble
initial d'états est {O, 1, 3, 7}. Les états 1, 3 et 7 ont chacun une transition sur le
caractère a vers les états 2, 4 et 7, respectivement. Comme l'état 2 est l'état
d'acceptation pour le premier modèle, on enregistre le fait que le premier modèle
(expression régulière a) convient après la lecture du caractère a. Mais, pour être
sûr d'avoir le lexème le plus long, on doit poursuivre les transitions jusqu'au bout.
En poursuivant ainsi le processus, on constate qu'à l'état 8, il n'y a plus de
transition possible sur le dernier caractère a ; on est donc dans un cas de blocage.
Comme la dernière concordance est arrivée après la lecture du troisième caractère,
on déduit que le troisième modèle, en l'occurrence a*b+, a reconnu l'entité lexicale
(lexème) aab.
Si on convertit l'automate fini non déterministe combiné (b) de la Figure 99,
on obtient la table de transition du Tableau XXIII, qui représente l'automate fini
déterministe équivalent [Aho, 86].

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

Tableau XXIII- Matrice de transition de l'automate fini déterministe

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.

On rappelle également que L ={lettre}, D ={digit} et LD =Lu D.


Tous les états annotés par le symbole dièse # jouent un double rôle. En effet, par
exemple, dans le cadre de la reconnaissance du mot-clé begin, à partir de l'état 1
et sur le caractère "e", l'automate transite vers l'état 2. Dans le cas contraire,
plutôt que d'avoir explicitement un arc sortant avec une lettre différente de "e",
l'état 1 se comporte comme l'état 14. De ce fait, l'entité prochaine à reconnaitre
ne peut être qu'un identificateur.
Par ailleurs, tous les états marqués par # sont des états finals, puisqu'on peut
aussi reconnaitre des identificateurs qui sont des préfixes des mots-clés begin et
end ou bien des identificateurs qui ont comme préfixe begin ou end. Par
exemple, à la rencontre d'un caractère qui n'est ni une lettre ni un chiffre (un
blanc par exemple), l'état 3 se comporte comme l'état 14, c'est-à-dire l'automate
transite vers l'état 15 et indique que le lexème beg a été reconnu en tant
qu'identificateur. A l'état 5 qui est censé être l'état final pour le mot-clé begin, il
est possible de rencontrer aussi une lettre ou un chiffre. Ainsi, au lieu de transiter
vers l'état 6 pour indiquer que le mot-clé begin a été identifié, l'automate transite
Analyse lexicale 191

plutôt vers l'état 14 pour poursuivre le processus de reconnaissance d'un


identificateur.

b
#
e # g # # # *
e: LD
:::::>
<begin>

e: LD
*

<end>

D
e: D *

<const, val>

LD *
e:LD

<ident, adr>

Figure 1 OO : Automate fini déterministe équivalent à celui de la Figure 96

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.

Le diagramme finalisé optimisé obtenu est celui de la Figure 101. Cet


automate est minimal, car chaque état final reconnait un lexème distinct de ceux
des autres états finals. Pour confirmation, on peut appliquer l'algorithme de
minimisation du nombre d'états d'un automate fini, donné en section 2 du
chapitre 2.
A l'issue de toutes ces opérations, il faut proposer une implémentation de
l'automate. Si on décide de construire le simulateur de l'automate sur la base de
la table de transition, il serait judicieux d'utiliser l'approche de compression de
192 Chapitre 5

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}

Figure 101 : Automate fini déterministe minimal équivalent à celui de la


Figure 100
Il faut cependant rappeler que cette manière de représenter les mots-clés par
leurs expressions régulières exactes n'est pas la seule option possible. En effet, on
peut très bien, comme il a déjà été mentionné plus haut, considérer un mot-clé
comme un simple identificateur, et c'est au moment de l'insérer dans la table des
symboles que l'on pourra savoir définitivement s'il correspond ou non à un mot-
clé.
Avant de décrire les grandes lignes sur la table des symboles et le traitement
des erreurs lexicales, il convient de donner un aperçu sur la génération
automatique d'analyseurs lexicaux.

8 Génération automatique d'analyseurs lexicaux


Plutôt que de construire manuellement un analyseur lexical, en considérant toutes
les questions soulevées par la phase de l'analyse lexicale, certaines tâches peuvent
être confiées à un outil spécialisé susceptible de générer automatiquement un
analyseur lexical capable de travailler comme un analyseur lexical codé
manuellement.
Analyse lexicale 193

Pour générer un tel analyseur on a besoin :


de modèles ou règles de spécification d'unités lexicales ;
de l'action à effectuer, sur l'identification de chaque unité lexicale.
Le programme générateur sera constitué :
d'un diagramme de transition construit à partir du modèle de spécification des
différentes unités lexicales ;
d'un fragment de code qui peut traverser n'importe quel diagramme de
transition ;
d'un code de spécification des actions respectives associées aux différentes
unités lexicales.
Le programme qui en résulte correspond à l'analyseur lexical envisagé. Il peut
donc être incorporé dans un compilateur. La Figure 102 illustre schématiquement
un générateur d'analyseurs lexicaux, ainsi que l'analyse lexicale d'un programme
source à l'aide de l'analyseur lexical produit par le générateur d'analyseurs
lexicaux.

spécification des unités générateur analyseur


lexicales avec les actions . . . d'analyseurs lexicaux . . . lexical
appropriées

(a) génération

programme analyseur reste du programme


source lexical compilateur cible

(b) compilation

Figure 102: Création d'un analyseur lexical et son incorporation dans un


compilateur

Pour illustrer la démarche préconisée dans la conception et la construction


d'un générateur d'analyseurs lexicaux, on considère d'abord l'exemple un peu
abstrait des deux entités suivantes :
l'ensemble des chaines constituées de zéro ou plusieurs a et se terminant par
un b.
l'ensemble des chaines formées de un ou plusieurs c.
Ces deux entités (lexèmes) sont formellement décrites respectivement par les
expressions régulières : a*b et c+. On peut donc formuler chaque modèle d'unité
lexicale, accompagné de l'action appropriée. On écrit alors :
a*b {écrire (l'entité 1 a été détectée)}
c+ {écrire (l'entité 2 a été détectée)}
194 Chapitre 5

Le formalisme des expressions régulières est utilisé d'ordinaire pour décrire


l'ensemble des chaines qui constituent les unités lexicales considérées. A partir de
cette description on construit l'automate fini déterministe correspondant. On
obtient ainsi le diagramme de la Figure 103. A l'issue de cette étape, on dispose
des éléments essentiels requis pour la génération automatique d'un analyseur
lexical, à savoir, la table de transition, la fonction pilote (simulateur du
fonctionnement de l'automate), ainsi que la fonction qui simule l'action
accompagnant chaque modèle d'unité lexicale.
Le même automate d'états finis (celui de la Figure 101), stocké sous forme
tabulaire dans une matrice nommée nextstate, est donné par le Tableau XXIV.
L'automate d'états finis, la fonction pilote nommée nexttoken, ainsi que la
fonction action, tous les trois regroupés, constituent l'analyseur lexical envisagé.

Figure 103: Diagramme de transition de l'automate d'états finis


déterministe pour les expressions régulières a* b et c+

~ 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;

Cette fonction ( nexttoken) est une version d'implémentation de l'algorithme


général de simulation d'un automate fini déterministe par sa table de transition
(voir section 7.4 de ce chapitre). Il est à noter que :
nextchar est la fonction qui renvoie le caractère suivant (caractère de pré-
vision) ;
final ( state) est un prédicat (fonction booléenne) qui teste si state est un état
final ;
nextstate [state, c] est l'état suivant de l'état state sur le caractère c ;
action est la fonction qui exécute l'action concernant le modèle d'unité lexicale
identifiée ;
unput ( c) permet de remettre le caractère lu dans le flot d'entrée ;
error est une routine de traitement des erreurs lexicales.
La partie action concernant les modèles de spécification des unités lexicales est
mise en œuvre par la fonction action:
function action ;
begin
case state of
2 : print ("l'entité 1 a été détectée") ;
3 : print ("l'entité 2 a été détectée") ;
196 Chapitre 5

end
end;

8.1 Aperçu sur le générateur d'analyseurs lexicaux Lex


Lex a été créé en 1975 par A. E. Lesk, des laboratoires Bell [Lesk, 1975]. La
version GNU 1 de Lex (pour le langage C), est appelée Flex (Fast Lex). A l'origine
c'est un outil d'Unix, aujourd'hui, on le rencontre aussi sous Windows. A la base
pour le langage C, aujourd'hui également pour Java. Il a été largement utilisé
pour spécifier des analyseurs lexicaux pour une grande variété de langages. Il est
souvent utilisé en conjonction avec Yacc: Yet another compiler compiler
(générateur d'analyseurs syntaxiques). L'outil Yacc possède également une
version GNU nommée Bison. La procédure de création d'un analyseur lexical à
l'aide de Lex est présentée à la Figure 104.
Lex est un outil qui génère automatiquement un analyseur lexical à partir
d'une spécification. Comme annoncé auparavant, il repose sur des expressions
régulières. Il prend en entrée un ensemble d'expressions régulières et génère en
sortie le texte source d'un programme C qui, une fois compilé, représente
l'analyseur lexical correspondant au langage défini par les expressions régulières en
question.
Plus précisément, comme montré sur la Figure 104, l'outil Lex reçoit en
entrée une spécification notée lex.l, exprimée dans le langage Lex et produit
(génère) en sortie un programme C lex.yy.c. Ce dernier consiste en une
représentation sous forme de tables de transition des expressions régulières de
lex.1, ainsi qu'une procédure standard permettant d'utiliser ces tables pour la
reconnaissance des lexèmes. Les actions associées aux expressions régulières dans
lex.l sont des segments de code C et sont recopiés directement dans le programme
lex.yy.c. Enfin, lex.yy.c est présenté à l'entrée d'un compilateur C et produit en
sortie un programme cible a.out qui représente l'analyse lexicale envisagée qui
prend en entrée un flot d'entrée et produit en sortie un ensemble d'unités
lexicales.
En somme, Lex est un utilitaire d'Unix (tout comme son frère Flex GNU),
accepte en entrée des spécifications d'unités lexicales sous forme de définitions
régulières et produit en sortie un programme écrit dans un langage de haut niveau
(ici le langage C) qui, une fois compilé, reconnait ces unités lexicales (ce
programme est donc un analyseur lexical).
Un fichier (programme) source pour Lex (Flex) est divisé en trois sections
séparées par deux lignes réduites à 3 3.
Première section (déclarations de variables, de constantes littérales et de
définitions régulières) présentée comme suit :
3{
déclarations (en C) de variables et de constantes ;

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}

Le symbole r est une expression régulière étendue et {action} est un fragment


de code C qui sera exécuté chaque fois qu'une unité lexicale satisfaisant
l'expression régulière r est reconnue. Comme mentionné ci-dessus, les actions des
règles de traduction peuvent faire appel aux expressions régulières de la première
section.
Enfin, dans la troisième section on rencontre toutes les procédures auxiliaires
qui pourraient être utiles dans les actions de la deuxième section. On pourrait
aussi compiler séparément ces procédures et les relier avec l'analyseur lexical.

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.

Expression Désignation Exemple


c tout caractère qui n'est pas un opérateur a
(ou méta-caractère)
\c caractère littéral c \*
"s" chaine littérale s "ab+*"
r1r2 r1 suivie de r2 ab
. tout caractère excepté fin de ligne a.b
~

comme premier caractère signifie le début de ligne ~abc

$ comme dernier caractère signifie la fin de ligne abc$


[s] tout caractère appartenant à la chaine s [abc]
1~ s] tout caractère n'appartenant pas à la chaine s [~abc]

r* zéro ou plusieurs occurrences de r a*


r+ une ou plusieurs occurrences de r a+
r? zéro ou une occurrence de r a?
r {p} p occurrences de r r {4}
r {p, q} p à q occurrences de r r {2, 5}
r1 1 r2 r1 ou r2 aJb
r1 / r2 r1 si elle est suivie de r2 abc/ad

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)

Il faut toutefois respecter certaines contraintes pour pouvoir formuler


correctement, sans ambiguïté, des expressions régulières sur la base de la syntaxe
des expressions Lex (Flex) listées dans le Tableau XXV. En effet :
les méta-caractères $, ~, et /, ne peuvent pas apparaitre dans ( ) , ni dans les
définitions régulières ;
le méta-caractère ~ perd son rôle d'indicateur de début de ligne s'il n'est pas
au début de l'expression régulière ;
le méta-caractère $ perd son rôle d'indicateur de fin de ligne s'il n'est pas à la
fin de l'expression ;
à l'intérieur des crochets [ ], seul le symbole \ (slash) reste un méta-caractère,
le symbole, - (tiret), ne le reste que s'il n'est ni au début ni à la fin dans [ ].
En ce qui concerne les règles de priorité selon lesquelles seront interprétées
certaines expressions, là-dessus, il va falloir également faire le point. En effet,
l'expression ivar 1 jconst* est interprétée comme (ivar) 1 (jcons(t*)) ; l'expression
xyz {1, 2} est interprétée avec Lex comme (xyz) {1, 2} et avec Flex comme xy(z
{1, 2}); l'expression ~ivar 1 iconst est interprétée avec Lex comme (~ivar) 1 iconst
et avec Flex comme ~(ivar 1 iconst).
Comme indiqué précédemment, les définitions régulières, selon la syntaxe Lex,
suivent le format suivant :
identifiant r
où identifiant est écrit au début de la ligne en colonne 0 (sans espace blanc
avant) et séparé de l'expression r par des espaces blancs. Par exemple :
chiffre [O - 9]
lettre [A- Z a - z]
Les identifiants ainsi définis peuvent être utilisés aussi bien dans les règles de
traduction que dans les définitions subséquentes ; il faut dans ce cas les encadrer
par des accolades. Par exemple :
Letterdigit {lettre} 1 {digit} est une définition régulière subséquente qui
fait référence aux identifiants lettre et chiffre définis ci-dessus.
De même:
idf {lettre}+( {lettre }I{ chiffre})*, est aussi une définition régulière
subséquente qui fait référence aux identifiants lettre et chiffre.
Les règles, quant à elles, comme décrites ci-dessus, sont de la forme :
www.bibliomath.com
200 Chapitre 5

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 */
}

Les deux procédures Rangerldf et RangerNbr définies dans la troisième section


et utilisées dans la deuxième section, seront recopiées textuellement dans lex.yy.c.
L'action associée à la règle de {idf} consiste en deux instructions : l'une est une
affectation à yylval de la valeur lexicale retournée par Rangeridf, l'autre est une
affectation du code de l'identificateur idf. Les nombres sont traités de la même
manière par la règle suivante. Dans les six règles suivantes, on utilise chaque
yylval pour retourner un code identifiant l'unité lexicale OPREL.
En recherchant un identificateur, s'il n'a pas encore été inséré dans la table des
symboles, la procédure Rangerldf crée une nouvelle entrée pour lui. Ainsi, les
yyleng caractères de l'entrée commençant à yytext peuvent être copiés dans un
tableau auxiliaire de caractères. La nouvelle entrée dans la table des symboles
peut pointer vers le début de cette copie.

Avant de clore ce volet, il convient d'ajouter quelques informations sur les


variables et fonctions prédéfinies de Lex ( Flex) :
FILE *yyin: fichier de lecture (défaut : stdin) ;
FILE *yyout : fichier d'écriture (défaut : stdout) ;
unput (char c) remet le caractère c dans le flot d'entrée ;
char yytext [ J : tableau de caractères qui contient le lexème qui a été accepté ;

www.bibliomath.com
202 Chapitre 5

int yytext : correspond à un pointeur vers le premier caractère du lexème


accepté;
yymore ( ) : fonction qui concatène la chaine actuelle yytext avec celle
reconnue auparavant ;
yyless ( ) : fonction admettant un entier comme argument, yyless (k>O), elle
supprime (yyleng-k) caractères de yytext [ ], ce qui revient à reculer le
pointeur yytext de (yytext-k) positions en arrière ;
int yyleng: longueur du lexème accepté;
int yylex ( ) : fonction qui lance l'analyseur Lex (et appelle yywrap ( )), elle
est active jusqu'au premier return ;
int yywrap ( ) : fonction appelée toujours en fin de flot d'entrée. Elle ne fait
rien par défaut, mais l'utilisateur peut la redéfinir dans la troisième section.
Elle retourne 0 si l'analyseur doit se poursuivre (sur un autre fichier d'entrée)
et 1 sinon ;
int main ( ) : la fonction main ( ) par défaut contient juste un appel à
yylex ( ). L'utilisateur peut la redéfinir dans la troisième section ;
int yylineno : numéro de la ligne courante (fonction Lex uniquement) ;
int yyterminate : fonction qui arrête l'analyseur (fonction Flex uniquement) ;
Il existe aussi une fonction spéciale notée ECHO équivalente à printf ( "3 s",
yytext). Par exemple :
[A - Za - z] [A - Za - zO - 9)* {printf ("3s", yytext) ; return IDF ;}
Le texte « printf (" 3s", yytext) » apparait très fréquemment dans les actions.
On peut l'abréger en ECHO, on a alors :
[A - Za - z) [A - Za - zO - 9)* {ECHO ; return IDF ;}

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.

8.2 Construction d'un automate fini déterministe à partir d'une


expression régulière
On va montrer, en s'appuyant sur un exemple, comment construire un automate
d'états finis déterministe à partir d'une expression régulière modifiée. Pour cela,
on considère l'expression (alb).bbalc+. Cette dernière peut être mise d'abord sous
forme d'un arbre abstrait décoré comme dans la Figure 105.

www.bibliomath.com
Analyse lexicale 203

1 2

Figure 105: Arbre abstrait décoré de l'expression régulière (alb)*bbalc+#

La décoration de l'arbre consiste en le marquage de ses nœuds comme suit :


Les feuilles sont annotées par des entiers qui représentent les positions (1, 2,
etc.), et des lettres majuscules A, B, etc. ;

Les nœuds internes sont étiquetés uniquement par des lettres majuscules A, B,
etc. ;

La concaténation est représentée par le symbole "•" ;

Pour repérer la fin de l'expression, on utilise le marqueur de fin #.

L'idée clé est d'identifier un état avec un ensemble de positions dans


l'expression régulière. Par exemple, à l'état initial, l'automate fini déterministe,
associé à l'expression régulière (alb)*bbalc+, peut s'attendre à rencontrer un a (en
position 1 de l'arbre correspondant), un b (dans la position 2 ou 3) ou un c (en
position 6). Par conséquent, cet état sera identifié par les positions 1, 2, 3 et 6,
que l'on note désormais par {1, 2, 3, 6}. A partir de ce dernier on peut obtenir les
transitions répertoriées dans le Tableau XXVI. Le diagramme de transition de la
Figure 106 est, bien entendu, une autre manière d'exprimer ces transitions (celles
du Tableau XXVI). En continuant de la sorte, on obtient finalement l'automate
d'états finis déterministe envisagé que l'on exprime par le diagramme de
transition de la Figure 107. Les états finals sont des états qui contiennent la

www.bibliomath.com
204 Chapitre 5

position 7, c'est-à-dire la position sur laquelle est attendu le symbole marqueur de


fin#.

si on est en sur le symbole on pourrait transiter à la position


position 1 a 1, 2, 3
position 2 b 1, 2, 3
position 3 b 4
position 6 c 6, 7

Tableau XXVI - Transitions à partir des positions 1, 2, 3 et 6 exprimées à


l'aide des positions des nœuds dans l'arbre de l'expression (alb)*bbajc+ #

Figure 1 06 : Diagramme de transition équivalent aux transitions du


Tableau XXVI

Figure 1 07 : Diagramme de transition finalisé de l'expression (al b) *bbal c+#

Pour formaliser la méthode de construction de l'automate, on introduit quatre


fonctions utiles : followpos, nullable, firstpos et lastpos basées sur des parcours
de l'arbre abstrait d'une expression régulière modifiée comme (ajb)*bbajc+#. Les
fonctions nullable, firstpos et lastpos sont définies sur les nœuds de l'arbre
www.bibliomath.com
Analyse lexicale 205

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.

si on est en alors sur le symbole on pourrait transiter à la position


position 1 a followpos (1) = {1, 2, 3}
position 2 b followpos (2) = {1, 2, 3}
position 3 b followpos (3) = {4}
position 6 c followpos (6) = {6, 7}

Tableau XXVII- Transitions à partir des positions 1, 2, 3 et 6 exprimées à


l'aide de lafonctionfollowpos dans l'arbre de l'expression (alb)°bbalc+#

Ainsi, les transitions des diagrammes deviennent comme celles de la


Figure 108, où les positions Pi à P1 sont les seules positions associées au symbole
a.

Figure 108 : Modèle de diagramme de transition basé sur la fonction


followpos

Comme préconisé, le calcul de followpos requiert de définir d'abord quelques


fonctions supplémentaires, à savoir : nullable, firstpos et lastpos. Ainsi :
nullable ( n) = vrai si la chaine représentée par n peut générer une chaine vide
(e). Donc, pour l'exemple de l'arbre de la Figure 105, on a nullable (D) =vrai
et nullable (F) = faux.

firstpos ( n) : firstpos d'un nœud n est l'ensemble des positions qui


correspondent à celles du premier symbole d'une certaine chaine dérivable à
partir de la sous-expression enracinée en n (de racine n). Par exemple, pour les
nœuds D et F on a firstpos (D) = {1, 2} et firstpos (F) = {1, 2, 3}.

lastpos ( n) : lastpos d'un nœud n est l'ensemble des positions qui


correspondent à celles du dernier symbole d'une certaine chaine dérivable à
partir de la sous-expression enracinée en n. Donc, pour les nœuds D et F on a
lastpos (D) = {1, 2} et lastpos (F) = {3}.
www.bibliomath.com
206 Chapitre 5

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).

Dans le Tableau XXIX, on récapitule tous les calculs concernant firstpos,


lastpos et follow pour tous les nœuds de l'arbre de la Figure 107 associée à
l'expression (al b) *bbal c+ #.

nœud n 1
nullable ( n} 1

n est une feuille étiquetée E vrai


n est une feuille étiquetée à la position i faux
n est c1 1 c2 nullable ( c1) ou nullable ( c2)
n est c1 • c2 nullable ( c1) et nullable ( c2)
n est c• vrai
n est c+ nullable ( c)

(a) Fonction nullable

nœud n 1
firstpos (n} 1

n est une feuille étiquetée E 0


n est une feuille étiquetée à la position i { i}
n est c1 1 c2 firstpos ( c1) u firstpos ( c2)
n est c 1 • c2 si nullable ( c1) alors
firstpos ( c1) u firstpos ( c2)
sinon firstpos ( c1)
n est c• firstpos ( c)
n est c+ firstpos ( c)

(b) Fonction firstpos

www.bibliomath.com
Analyse lexicale 207

nœud n lastpos (n)


n est une feuille étiquetée e 0
n est une feuille étiquetée à la position i { i}
n est c1 1 c2 lastpos ( c1) u lastpos ( c2)
n est c1 • c2 si nullable ( c2) alors
lastpos ( c1) u lastpos ( c2)
sinon lastpos ( c2)
n est c• lastpos ( c)
n est c+ lastpos ( c)

(c) Fonction lastpos

Tableau XXVIII- Règles de calcul des fonctions nullable, firstpos et lastpos

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+ #.

nœud firstpos lastpos followpos


A {1} {1} {1, 2, 3}
B {2} {2} {1, 2, 3}
c {1, 2} {1, 2}
D {1, 2} {1, 2}
E {3} {3} {4}
F {1, 2, 3} {3}
G {4} {4} {5}
H {1, 2, 3} {4}
I {5} {5} {7}
J {1, 2, 3} {5}
K {6} {6} {6, 7}
L {6} {6}
M {1, 2, 3, 6} {5, 6}
N {7} {7}
0 {1, 2, 3, 6} {7}

Tableau XXIX- Résultats obtenus en appliquant les fonctions nullable,


firstpos et lastpos sur l'expression régulière (al b) *bbal c+#

www.bibliomath.com
208 Chapitre 5

Algorithme de construction de l'automate fini déterministe pour


(aJb)*bbaJc+#. Cet algorithme permet de construire l'automate de la Figure 107.
Construire un arbre abstrait pour l'expression r#.
Construire les fonctions nullable, firstpos, lastpos et followpos.
Empiler firstpos (racine) /* firstpos (racine) est l'état initial*/;
tantque -, pile-vide faire
début
dépiler l'état U;
pour chaque symbole d'entrée a faire
début
soient Pt, p2, ... pk les positions dans U
correspondant au symbole a ;
soit V= followpos (Pt) u ... u followpos (Pk);
empiler V dans la pile s'il n'est pas marqué
et non déjà dans la pile ;
établir la transition de U vers V étiqueté par a
fin
fin

9 Table des symboles


Tout au long du processus de compilation, il est nécessaire d'avoir une table des
symboles où peuvent être mémorisées les entités collectées au cours de l'analyse
lexicale. Cette table est une structure de données accessible au cours de toutes les
phases de compilation. Elle constitue en quelque sorte la mémoire du compilateur.
On doit pouvoir y accéder à tout moment pour y rechercher ou y ajouter une
entité ou une information relative à cette entité. Elle doit contenir les noms des
variables (identificateurs), mais peut également contenir les identificateurs des
fonctions, des procédures, les constantes, les mots-clés, éventuellement même les
étiquettes.
La référence à un identificateur déclenche systématiquement une recherche
dans la table des symboles ; il est alors primordial que l'accès à la table des
symboles soit rapide. On décrira dans ce qui suit quatre modes d'accès bien
connus parmi les structures de données classiques existantes.
Les entrées dans la table des symboles ont généralement la forme de la
structure composée de (nom, attributs), où les attributs peuvent être : le type, la
valeur, l'adresse, l'étendue, l'adresse d'une routine en cas d'erreur, etc. On
donnera de plus amples informations à ce sujet dans la section 9.2 du présent
chapitre, mais avant cela on s'intéresse d'abord aux modes d'accès existants.

9.1 Accès à la table des symboles


A cc ès linéaire ou séquentiel
Les lexèmes sont insérés selon l'ordre de leur apparition dans le flot d'entrée. A la
rencontre d'un identificateur dans un programme source, la table est parcourue
séquentiellement. Si l'identificateur n'est pas déjà présent parmi les i - 1 entrées
www.bibliomath.com
Analyse lexicale 209

inspectées, il sera logé à la prem1ere place libre i. En bref, la recherche d'un


élément nécessite de parcourir séquentiellement la table de taille n, ce qui donne
une complexité (nombre d'accès) de l'ordre 0( n). L'insertion d'un identificateur
dans la table, tout comme la recherche, nécessite un nombre d'accès de O(n).
Cette technique est simple à mettre en œuvre, mais requiert un temps d'accès
relativement long. Elle n'est efficace que si la table est de petite taille.
L'implémentation de cette table nécessite généralement une structure de données
statique, mais, à cause de la limite de la taille de la table, le risque de saturation
existe lorsque la table est insuffisamment dimensionnée. On peut, alternativement,
envisager l'utilisation d'une structure de données dynamique avec des entrées
chainées par des pointeurs, mais ça sera au prix d'un temps d'accès plus long. En
effet, chaque entrée a un champ supplémentaire représenté par un pointeur qui
freine relativement le parcours. Seule la mémoire de la machine sur laquelle tourne
le compilateur risque de restreindre la taille de la table.
Accès dichotomique
Avec la technique basée sur l'accès dichotomique, la table est forcément ordonnée
(triée). La technique consiste d'abord à diviser la table de taille n en deux parties,
ensuite à regarder si l'identificateur recherché correspond à celui du milieu de la
table ; si c'est le cas on s'arrête, sinon on cherche dans quelle moitié pourrait ou
devrait se placer l'identificateur rencontré. En fonction du résultat, le processus de
recherche peut recommencer dans cette moitié, et ainsi de suite. La procédure
recherche_ dicho donne une idée sur la technique de la recherche dichotomique.
procedure recherche_ dicho ( ) ;
begin
i := 1 ; j := n ; / * n est ici la taille maximum de la table * /
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 k := 0 / * l'identificateur n'a pas été trouvé */
end;
La complexité de la recherche dichotomique dans le pire des cas est O(log2 n).
L'opération d'insertion dichotomique est subordonnée à l'opération de recherche ;
sa complexité est au pire de l'ordre 0( n). Ceci s'explique par les déplacements des
identificateurs à l'intérieur de la table afin de pouvoir placer un nouvel
identificateur à la place appropriée dans le but de maintenir la table triée. La
procédure dichotomique suivante prend en charge conjointement la recherche et
l'insertion dans le cadre de l'accès dichotomique. Dans l'algorithme suivant le
nombre n représente la taille courante de la table, ce qui correspond au nombre
d'identificateurs déjà insérés. On note par taille la taille maximum de la table.
www.bibliomath.com
210 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;

On remarque que si entrée[ n]. identificateur <nom, 1'insertion de


l'identificateur nom est réalisée en 1 seul accès. Dans le cas contraire, la recherche
nécessite au plus log2 n comparaisons. Cependant, l'insertion d'un élément se fait
en un temps linéaire, c'est-à-dire qu'il faut log 2 n comparaisons pour trouver la
position de l'élément à insérer, plus une moyenne de n/2 décalages vers la droite
des identificateurs de la table plus grands que l'identificateur à insérer. Autrement
dit, la complexité au pire est de l'ordre O(n). Par conséquent, ce mode d'accès
n'est avantageux que si les insertions sont plus rares que les recherches.
L'insertion serait plus facile si l'on remplaçait la table par une liste chaînée : elle
se ferait en un temps constant, une fois la position trouvée. Ceci fait penser
directement à une autre technique d'accès nommée accès par arbre binaire.
Accès par arbre binaire ordonné
Un arbre binaire est un arbre formé de nœuds qui ont au plus deux (0, 1 ou 2)
fils. On appelle ces fils «gauche» et « droit ». On s'intéresse ici à l'accès basé sur
la structure de l'arbre binaire de recherche, c'est-à-dire un arbre binaire ordonné.
www.bibliomath.com
Analyse lexicale 211

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

identificateur est toujours mémorisé ou retrouvé aussi rapidement. Les tables de


hachage permettent un accès de l'ordre 0(1), quel que soit le nombre d'éléments
dans la table. Les tables de hachage sont surtout utiles lorsque le nombre
d'entrées est très important. Ce mode d'accès est souvent désigné par hachage ou
technique de rangement dispersé. La technique est aussi appelée « hash-coding »,
accès par tables mêlées ou « hash tables », etc.
La technique repose sur l'utilisation des caractères de l'identificateur pour
calculer l'adresse ou l'entrée dans la table.
Il existe plusieurs algorithmes plus ou moins efficaces pour calculer h ( J) ; on se
limite ici à en citer quelques-uns :
On calcule la somme arithmétique des caractères qui constitue l'identificateur ;
On calcule une somme arithmétique pondérée des caractères de
l'identificateur ; le poids peut être la position du caractère dans
l'identificateur ;
On multiplie la valeur de h par une constante a avant d'y ajouter le caractère
suivant ; autrement dit, cela revient à considérer ho = 0 et hi= Œ1ti-1 + Ci pour
1 $; i $; k et à prendre h = hk où k est la longueur de l'identificateur.
On multiplie l'identificateur par lui-même et on extrait quelques bits du
milieu;
On prend le « ou exclusif » des différents caractères au lieu de les additionner ;
etc.
Pour tous ces algorithmes, on calcule le reste de la division de h ( J) par la
taille maximum de la table. Mais, une fois calculée ce reste qui représente l'entrée
dans la table des symboles pour l'identificateur /, on peut être confronté au
problème des collisions. En d'autres termes, il existe toujours des identificateurs
distinctes 11 et 12 pour lesquelles on a h (11) = h (12 ). Une telle situation est
appelée collision entre 11 et 12. Par conséquent, pour utiliser à bon escient une
table à rangement dispersé ou, d'une façon générale, les méthodes de hachage,
l'utilisateur doit définir :
une fonction de hachage ;
une méthode de résolution des collisions.
Il existe deux stratégies principales pour résoudre les collisions : la stratégie
basée sur le rehachage (ou le sondage) et la stratégie basée sur le chainage.
Rehachage
Quand une entrée E de la table subit une collision par l'arrivée d'un nouvel
identificateur, on essaie une nouvelle entrée (E + R 1 ) mod n; si elle est
occupée, on essaie encore ( E + R2) mod n, etc.
Quelques méthodes de rehachage couramment utilisées sont :
• Le rehachage linéaire, Ri= i où i = 1, 2, ... m ...
• Le rehachage linéaire par nombre premier Ri = p où p est un nombre
premier;
• Le double hachage Ri = h où h est une nouvelle fonction de hachage ;
• Le rehachage quadratique Ri= ai2 + ~i + y, avec a, ~. y sont des
nombres premiers.
www.bibliomath.com
Analyse lexicale 213

La stratégie de rehachage permet un adressage ouvert et consiste, dans le cas


d'une collision, à stocker les valeurs de hachage dans des cases contigües de la
table. De ce fait, suite à une collision sur l'entrée E, si on essaie les nouvelles
entrées (E + R 1) mod n, ... , (E + Rk) mod n, etc., sans succès, et si (E + R1 +
R2 ... + Rk) mod n = E, cela signifie que la table est saturée.
La Figure 110 donne un aperçu sur la table des symboles selon l'approche
basée sur la stratégie de rehachage.

identificateurs indices identificateurs descripteur


0
sara
1 lis a
1

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.

identificateurs indexes liens identificateurs descripteur


0
sara
1

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

En somme, pour résumer un peu la différence entre le rehachage et le chainage,


on se propose d'insérer la séquence d'identificateurs : lisa, sara, dani, sali et asra.
Pour cela, on utilise la fonction de hachage basée sur la somme arithmétique des
codes ASCII des caractères des identificateurs. En cas de collision, on utilise, par

www.bibliomath.com
Analyse lexicale 215

exemple, le rehachage quadratique si on opte pour le rehachage, et le chainage


interne si on opte pour la stratégie de chainage. On suppose que la taille
maximum de la table est égale à 11. Il semble que l'on obtient de meilleurs
résultats si cette taille est un nombre premier. Les tailles des tables de hachage
sont souvent des nombres premiers, afin d'éviter les problèmes de diviseurs
communs, qui engendreraient un nombre important de collisions. Une alternative
est d'utiliser une puissance de deux, ce qui permet de réaliser l'opération modulo
par de simples décalages, et donc de gagner en rapidité.
On utilise les codes ASCII décimaux des caractères ; on a donc ord ("a") = 97,
ord ("d") = 100 ; ord ("i") = 105 ; ord ("1") = 108 ; ord ("n") = 110 ;
ord ("r") = 114; ord ("s") = 115.
Compte tenu des considérations (modulo la taille de la table), concernant le calcul
de la fonction de hachage, les valeurs respectives des entrées associées aux
identificateurs à insérer sont h (lisa) = 425 mod 11 = 7, h (sali) = 7, h (sara) =
423 mod 11 = 5, h (asra) = 5, h (dani) = 412 mod 11 = 4. En cas de collision
entre "lisa" et "sali" ou entre "sara" et "asra", on applique, par exemple, la
fonction de rehachage quadratique Ri= i 2 + 2i + 3. Pour i = 1, on a Ri= 6 ; ce
qui donne les entrées (7 + 6) mod 11 = 2 et (5 + 6) mod 11 = 0, respectivement
pour "sali" et "asra" sur la Figure 113.

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.

9.2 Organisation de la table des symboles


Contrairement à l'accès qui peut être choisi pour sa performance et/ou sa
simplicité d'implémentation, l'organisation de la table des symboles dépend de
certains paramètres comme la nature du langage du compilateur (structuré en
blocs éventuellement imbriqués, mots-clés reservés ou non, etc.), mais aussi du
type de compilateur (multi-passe ou une seule passe), à construire.
En effet, par exemple, si les mots-clés sont reservés, le compilateur doit interdire
de les utiliser comme variables de l'utilisateur. Par conséquent, s'ils doivent
figurer dans la table des symboles, ils faut les installer avant de démarrer
l'analyse. S'ils sont interceptés par l'analyseur lexical (décrits par des diagrammes
de transition), il est inutile de les insérer dans la table des symboles. Si, au
contraire, les mots-clés ne sont pas reservés, ils doivent impérativement être
installés dans la table des symboles avec la mention de leur utilisation en tant que
mots-clés. Si le langage est à structure de blocs, on doit mentionner le numéro de
bloc ou de niveau dans l'entrée de la table pour un nom. De même, il peut arriver
qu'un même nom puisse dénoter plusieurs objets distincts, y compris dans un
même bloc ou une même procédure. Dans un tel cas, l'entrée dans la table
concernant chaque objet n'est créée que si on est sûr du rôle joué par le nom qui
le dénote.
Si le compilateur est en une seule passe, il est inutile de ranger les constantes
dans la table des symboles, l'analyse lexicale renvoie un couple : <unité lexicale,
valeur d'attribut> aux autres phases du compilateur.
Chaque entrée dans la table des symboles correspond à la rencontre d'un nom
dans le flot d'entrée. Le format des entrées de la table des symboles n'est pas
forcément uniforme. En effet, les informations concernant un nom qui y sont
rangées dépendent de l'utilisation de ce nom. Chaque entrée dans la table des
symboles d'une entité peut être implantée par un enregistrement formé d'une
séquence de mots consécutifs en mémoire. Pour préserver l'uniformité des
enregistrements de la table des symboles, il peut être commode de conserver hors
de son entrée dans la table une partie des informations concernant un nom, en ne
mettant dans l'enregistrement qu'un pointeur vers ces informations. A titre
d'exemple, s'il existe une limite supérieure raisonnable pour la longueur des noms
(identificateurs, mots-clés, etc.), les lexèmes correspondants doivent être stockés
dans la table des symboles, comme on peut le voir sur le Tableau XXX. En
revanche, s'il n'y a pas de limite à la longueur possible ou si cette limite n'est
jamais atteinte, on peut utiliser le procédé de rangement indirect comme celui de
la Figure 114, où est définie la table des noms, où sont rangés les suites de
caractères correspondant effectivement à ces noms.
Pour rappel, la zone « Attributs » de la table dans le Tableau XXX
correspond à ce que l'on a dèjà nommé plus haut en début de section 9, le type ou

www.bibliomath.com
Analyse lexicale 217

le code correspondant à unité lexicale, la valeur, l'adresse, l'étendue, l'adresse


d'une routine en cas d'erreur, etc.

Nom Attributs
~-~------------------------------------------------------------------·

>-------<------------------------------------------------------------------
div
f---~:------------------------------------------------------------------
mod
count ;------------------------------------------------------------------.
f---~,------------------------------------------------------------------:

' '
~-----<------------------------------------------------------------------·

Tableau XXX- Stockage des caractères qui forment un identificateur


dans une zone de taille fixe de son entrée

Lien Unité lexicale -----------------------------------,


Reste de la zone Attributs
~
-: :._-: _
-----~f-- =_1+~3_d~iv~>~~~------=·-----------------------------------~
'
1 - : ._._ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

'
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 # #

Figure 114 : Stockage des caractères qui forment un identificateur dans


un tableau séparé nommé table des noms

L'utilisation de pointeurs est certes coûteuse en temps et en espace, mais dans


le contexte de la construction d'un compilateur, elle s'avère utile, voire même
rentable.
En ce qui concerne les identificateurs 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), il faut prévoir :
Une seule entrée dans la table des noms, lorsque les caractères qui forment
l'identificateur sont stockés dans un tableau séparé comme sur la Figure 114 ;
Plusieurs entrées dans la table des symboles.
Toutefois, la table des symboles doit permettre de « voir » le bon
identificateur parmi plusieurs autres portant le même nom.

Par exemple, pour la déclaration en C :


int X;
struct x {float y, z ;} ;
deux entrées dans la table des symboles seraient creees pour l'identificateur x:
une désignant x comme un entier et l'autre le désignant comme une structure.
218 Chapitre 5

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 ·

proced ure pro c2;


var i: integer; ' bloc 2
procedure proc3; bloc 3
var j, k : boolean 1

On peut adopter une des approches suivantes :


La solution qui utilise la numérotation des niveaux et des blocs pour repérer
chaque identificateur par son numéro de bloc et son numéro de niveau. La
Figure 115 donne une vue générale sur l'organisation de la table des symboles
avec cette approche.

lien token bloc niveau type utilité reste de Attributs


'
-- -------,
••• 1

<idf> 0 0 integer var xdu pp :_________ _,


<idf> 0 0 proc procédure procl :
---------,
...
:_________ _.'
<idf> 1 1 real var x de procl ---------..,
:_________ _.
<idf> 3 2 bool var j de proc3 :
--------- ...
:
----------·

# r o c 1 # j #

Figure 115 : Table des symboles par niveaux et par blocs

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

• On dépile les entités locales au bloc ou à la procédure à la sortie du bloc ou


de la procédure.

La Figure 116 donne le schéma en pile de la table des symboles.

Sommet de pile -----+ y variable real


X variable real
pro cl procedure
J variable integer
X variable integer
a variable integer
fou rien pro gram

Figure 116 : Table des symboles en pile

L'inconvénient est que si le compilateur est en plusieurs passes l'information


est perdue. Cette alternative n'est donc valable que dans le cas d'un compilateur à
une seule passe.
Enfin, la solution qui permet la gestion de la table des symboles de manière
arborescente, c'est-à-dire selon l'imbrication des blocs et/ou des procédures.
Pour simplifier, on suppose qu'à chaque procédure ou bloc est associée une
table des symboles. Une implantation possible d'une table des symboles est
une liste chainée d'entrées pour les noms. Ainsi, la nouvelle table des symboles
pointe vers la table englobante. La Figure 117 montre une représentation de
la table des symboles correspondant au programme fourien et les trois
procédures procl, proc2 et proc3. La structure hiérarchique (imbriquée) des
procédures peut être déduite du chainage entre les tables qui constituent la
table des symboles totale.
Les tables des symboles des procédures procl et proc2 pointent vers la table
de fourien, la procédure englobante. La table de la procédure proc3, quant à elle,
pointe vers la table de proc2, puisque proc3 est déclarée à l'intérieur de proc2.
Dans la zone entête on y trouve des informations utiles comme le niveau
d'imbrication de la procédure, le pointeur vers la table de la procédure
englobante. On peut également numéroter les procédures dans l'ordre suivant
lequel elles sont déclarées, et conserver ce numéro dans l'entête.

Il est prématuré de rentrer dans tous les détails concernant le contrôle et


l'organisation totale de la table des symboles au niveau de l'analyse lexicale,
particulièrement, lorsque le compilateur est multi-passe, car il est impossible de
connaitre les attributs d'une entité (variable, procédure, fonction, etc.), au cours
de l'analyse lexicale toute seule. Il va donc falloir attendre les phases suivantes,
pour enfin déterminer les attributs de l'entité en question. Les valeurs d'attributs
sont ajoutées au fur et à mesure que les informations deviennent disponibles. Si le
langage admet que les procédures puissent être imbriquées, les identificateurs
220 Chapitre 5

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

Figure 117 : Tables des symboles pour des procédures imbriquées

Par exemple, avec la déclaration en C :


int X;
struct x {float y, z ;} ;
où l'identificateur x est à la fois un entier et une structure, un analyseur lexical
isolé n'a aucune information supplémentaire pour distinguer entre les deux
entités ; l'analyse lexicale toute seule ne voit la variable x que comme une et une
seule entité. Autrement dit, il va falloir que ce genre de conflit soit résolu dans un
cadre global, où l'analyse lexicale est, par exemple, une coroutine de l'analyse
syntaxique.
La table des symboles doit être réalisée avec soin, on estime qu'un compilateur
passe la moitié de son temps à la consulter. Le remplissage de cette table (la
collecte d'informations) a lieu lors des phases d'analyse. Les informations
contenues dans une table de symboles sont requises lors des phases d'analyses
syntaxique et sémantique, ainsi que lors de la génération de code. Par exemple, au
cours de l'analyse lexicale, un lexème représentant un identificateur est
sauvegardée dans une entrée de la table des symboles. Les phases suivantes du
Analyse lexicale 221

compilateur peuvent ajouter à cette entrée des informations (attributs), comme le


type de l'identificateur, son utilisation (variable, procédure, fonction ou étiquette)
et son adresse en mémoire. La phase de génération de code pourra alors utiliser
ces informations pour produire le code propre permettant d'accéder à cette
variable.
En somme, la table des symboles est construite lors des phases d'analyse et
utilisée lors de la génération de code.

10 Traitement des erreurs lexicales


L'analyse lexicale a une vision très réduite du programme source. Autrement dit,
l'analyse lexicale s'intéresse à la syntaxe de chaque lexème pris isolément
indépendamment de son environnement immédiat (instruction, bloc, déclaration,
etc.). Par conséquent, très peu d'erreurs sont détectables au niveau lexical.
Une erreur lexicale se produit lorsque l'analyseur lexical rencontre un lexème
qui ne correspond à aucun modèle d'unité lexicale, envisagé. Une erreur lexicale
peut être, l'écriture erronée d'un identificateur, d'un mot-clé ou opérateur. Par
exemple, dans un programme source Pascal, si l'analyseur lexical rencontre le
lexème 11 esle 11 , s'agit-il du mot-clé 11 else 11 mal formé ou tout simplement d'un
identificateur ? Dans ce cas précis, comme le lexème correspond au modèle de
l'unité lexicale <idf, adr> (identificateur), l'analyse lexicale ne détectera pas
d'erreur, et retournera à l'analyseur syntaxique un <idf, adr>. En revanche, s'il
s'agit d'un mot-clé mal orthographié, c'est à l'analyseur syntaxique de détecter
l'erreur. Autre exemple : si l'analyseur lexical rencontre le mot lX, il ignore
totalement, s'il s'agit d'une erreur de typographique, auquel cas l'utilisateur
aurait, par exemple, pensé écrire Xl, X, 1 ou tout autre chose. Dans un tel cas,
l'analyse lexicale peut juste signaler que le lexème lX ne correspond à aucun
modèle d'unité lexicale, envisagé.
L'analyseur lexical peut détecter des erreurs et les signaler par des messages
explicites susceptibles d'aider l'utilisateur à corriger son programme. Par exemple,
l'analyseur peut :
signaler qu'un lexème est mal formé, et donne le numéro de ligne où il
apparait dans le programme source ;

trouver aussi qu'un lexème est trop long et signaler cette anomalie ;

signaler un dépassement de capacité lors du calcul de la valeur d'une


constante;

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

signaler l'erreur par un message d'avertissement explicite ;


laisser tomber le lexème erroné et passer à la suite.
Mais, cela peut gêner parfois le compilateur qui peut engendrer des « erreurs
secondaires » imputables à la stratégie « mode panique » adoptée.
Par exemple, une erreur dans une zone déclarative comme :
var lX, Y, Z : integer ;

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 d'analyse syntaxique

Non déterministes Déterministes

Analyse basée sur toutes les Analyse basée sur


grammaires de type 2 certaines grammaires
spéciales de type 2

Méthodes générales Méthodes


tabulaires
Méthodes Méthodes
descendantes ascendantes
basées sur des basées sur des
grammaires LL grammaires LR

Méthodes
Méthode de Méthode
ascendantes basées
Coke Younger d'Earley
sur des grammaires
Kasami
de précédence

Figure 118: Aperçu de classification des méthodes d'analyse syntaxiques

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.

Il est donc impératif de toujours se ramener à une grammaire acyclique et sans


récursivité à gauche pour contourner le problème des boucles sans fin dans les
analyseurs descendants, et de se ramener toujours à une grammaire acyclique pour
éviter les boucles sans fin dans les analyseurs ascendants. D'autres contraintes
seront ajoutées autant que nécessaire sur les grammaires utilisées afin de
contourner toute forme de non déterminisme et, du coup, rentabiliser le processus
d'analyse.

Au terme de cet aperçu, il convient à présent d'introduire certains éléments


théoriques indispensables afin de pouvoir définir les concepts de grammaires LL,
LR et de précédence. C'est l'objet de la section 2 de ce chapitre.

2 Eléments théoriques de base

2.1 Grammaires LL(k)


« left to right parsing », « left analysis »; c'est-à-dire lecture de gauche à droite
pour obtenir l'analyse gauche (descendante). Le terme k étant le nombre de
caractères en pré lecture ou prévision (k-lookahead). On note que k ~ l.

Définition 2.1 (Ensemble Firsh (a.))


On considère une grammaire à contexte libre G = (VN, VT, P, S).
On définit le k-préfixe de l'élément a. e (VT uVN) * par l'ensemble Firstk (a)
représenté par le formalisme suivant :
Analyse syntaxique 227

Firstk (a) = {x a ~· 1 x{J et lxl = k ou a ~· 1 x et lxl < k}


1

lxl = k OU lxl < k, signifie que XE V;k, ~ E (VT UVN)*


Par exemple, soit la grammaire dont les règles sont :
S~T+SIT
T ~a 1 (S)
On donne k = 1 et on calcule les ensembles First1 pour les symboles Set T.
First1 (S) = First 1 (T + S) u First 1 (T)
First 1 (T) = First1 (a) u First 1 ((S)) ={a, (}
First1 (T+ S) = First1 (T). First 1 ( +T) = {a, (}. Donc, First 1 (S) = {a, (}

Définition 2.2 (Ensemble Followk(~))

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) *

On reconsidère la même grammaire et on procède au calcul de Followk pour


k = 1. On calcule les Follow1 des symboles S et T.
Follow1 (S)
S => T + S, d'après la définition on a ~ = E ; donc E E Follow1 (S)
S => T => (S) d'après la définition on a ~ = ) ; donc ) E Follow1 (S)
Il n'y a pas d'autre élément à calculer ; donc Follow1 (S) = {E, )}
Follow1 (T)
S => T + S, d'après la définition on a ~ = +S ; donc +E Follow1 (T)
S => T, d'après la définition on a~ = E; donc E E Follow1 (T)
S => T => (S), d'après la définition on a ~ =) ; donc, ) E Follow1 (T)
Il n'y a pas d'autre élément ; donc Follow1 (T) = { +, ), E}.

Un deuxième exemple de calcul des First1 et Follow1 est celui de la grammaire


définie par les règles suivantes :
E~TM

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

Tableau XXXI - Ensembles First1 et Follow1

Condition LL(l) (Théorème)


La CNS (condition nécessaire et suffisante) pour qu'une grammaire
G = VN, VT, P, S) soit LL{l) est que V A-+ a 1 1 a 2 E P, on a toujours :
First 1 (a 1 . Follow1 (A)) n First1 (a 2 . Follow1 (A)) = 0
La grammaire donnée ci-dessus dont les règles sont les suivantes n'est pas
LL {1).
S~T+SIT
T ~a 1 (S)
En effet, pour T ~ a 1 {S), on a :
First1 (a.Follow1 (T)) n First1 ((S).Follow1 (T)) = {a} n {(} =0 qui satisfait
le théorème, alors que pour S ~ T+ S 1 Ton a:
First1 ((T+S).Follow 1 (S)) n First1 (T. Follow1 (S)) = {a, (}n {a, (}-:;:. 0 qui ne
satisfait pas le théorème.
Donc, comme annoncé ci-dessus, la grammaire n'est pas LL{l).
On peut exprimer de manière plus explicite et détaillée la condition LL{l). On
dira dans ce cas qu'une grammaire de type 2 est LL{l) si et seulement si les 3
conditions suivantes sont vérifiées.
La grammaire ne doit pas être récursive à gauche
En effet, la récursivité à gauche constitue un handicap majeur pour les analyseurs
descendants, qu'on nomme aussi parfois analyseurs gauches. La raison en est que
l'analyseur basé sur une telle grammaire peut boucler indéfiniment.
Par exemple, si on est appelé, au cours de l'analyse, à appliquer les règles
A ~ a~ 1 Aa, et que si à la place du symbole a on rencontre un autre symbole, le
changement de la règle A ~ a~ par son alternative A ~ Aa., ne résout rien. Bien
au contraire, cela fera rentrer l'algorithme d'analyse dans une boucle sans fin.
D'où, la nécessité d'éliminer cette récursivité à gauche pour tout analyseur
descendant {même s'il s'agit d'un analyseur descendant non déterministe).
V A-+ a 1 1 a 2 E P avec a 1 -::f:. a 2 First 1 (a1 ) n First 1 (a2 ) = 0
L'alternative a,1 doit nécessairement commencer par un symbole différent de celui
de a2. Le contraire aurait provoqué un conflit, car si a1 = aô et a2 = ay, on ne
Analyse syntaxique 229

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, (

Tableau XXXII- Calcul des ensembles First 1 et Follow1 modifié


Par exemple, la grammaire donnée précédemment, définie par les règles de
production suivantes :
E~TM

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

avec S comme axiome.


Cette grammaire est récursive à gauche, car elle possède la règle A ~ Abc.
Donc, en l'état, cette grammaire n'est pas LL(l). On peut toutefois se ramener à
une grammaire LL(l) si le langage qu'elle engendre le permet. On élimine d'abord
la récursivité à gauche, ensuite on procède par factorisation s'il y a lieu.
En éliminant la récursivité à gauche dans les règles A ~ Abc 1 b, on obtient
les quatre nouvelles règles :
A~ b 1 bC
C ~be 1 bcC
Après factorisation, on obtient les règles :
A~bD
D~el C
C~bcD

qui se transforment en les règles :


A~bD
D ~ e 1 bcD.
La grammaire finale obtenue est donc celle définie par les règles suivantes :
S ~ aABe
A~bD
B~d
Analyse syntaxique 231

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).

Condition LL(k) avec k ~ 2 (Théorème)


La CNS (condition nécessaire et suffisante) pour qu'une grammaire
G = (VN, VT, P, S) soit LL(k} avec k ~ 2 est que V A--+ a 1 1 a 2 E P, et la
chaine dérivable wAy obtenue par S ~· 1 wAy, on a toujours :
Firstk(a 1 y) n Firstk(a 2 y) = 0.
Par exemple, soit à vérifier si la grammaire définie par les règles suivantes :
S --7 OAOO 1 lAlO
A--7llE
est LL(2).
Il suffit d'appliquer scrupuleusement le théorème précédent. En effet :
Pour S on a les productions OAOO et lAlO qui commencent respectivement par
0 et 1 qui sont distincts. First 2 (OAOO) = {OO, 01} ; First 2 {lAlO) = {11}. La
seule chaîne dérivable wAy à partir de S, c'est le symbole S lui-même d'après la
dérivation S =>o S ; car le symbole S n'apparaît nulle part à droite dans les
autres règles de production. On a donc w = E, A = S, y = E., et par
conséquent, on obtient :
First 2 {OAOO) n First 2 {lAlO) = {01, OO} n {11} = 0.

En ce qui concerne A, on a A --7 1 1 f. , c'est-à-dire a.1 = 1 et a.2 = E., ce qui


donne S => OAOO; y= OO; w = 0; car wAy = OAOO; alors,
First 2 (1.00) n First 2 {E..00) = {10} n {OO} = 0.
De même, on a :
S => lAlO ; y= 10 ; w = 1 ; car wAy = lAlO ; donc, on a aussi :
First 2 (1.10) n First 2 (E.10) = {11} n {10} = 0.
Le théorème est vérifié avec k = 2, donc la grammaire donnée est LL(2).
232 Chapitre 6

Grammaire LL(k) forte (Théorème)


La CNS (condition nécessaire et suffisante) pour qu'une grammaire
G = (VN, VT, P, S) soit LL{k) forte {Strong LL{k) Grammar) est que
V A ~ a 1 1 a 2 E P, on a toujours :
Firstk(a 1 .Followk(A)) n Firstk(a 2.Followk(A)) = 0

Par conséquent, toute grammaire LL(l) est forte.


Par exemple, soit à vérifier si la grammaire 11(2) précédente, définie par les
règles :
S ~ OAOO j lAl
A~ 1jE
est 11(2) forte.
On peut vérifier aisément si elle est forte en calculant les ensembles Follow 2 de
Set A.
Ainsi, Follow 2 (S) = {E} et Follow2(A) = {OO, 10}. Donc, pour S, on a First 2
(OAOO.{ E}) n First2 (lAlO.{ E}) = {01,00}n{ll} = 0
Pour A, First 2 (1.{00,10} )n First 2 ( E.{00,10}) = {10,ll}n{00,10} = {10}:;:. 0.
Le théorème n'est pas vérifié, par conséquent, G n'est pas 11(2) forte.
En revanche, la grammaire définie par les règles suivantes :
S ~ E j abA
A~ Saa 1 b
est 11(2) forte.
En effet, Follow 2 (S) = {E, aa} et Follow 2 (A) = {E, aa}, et on a:
Pour S, First 2 (E.{ E, aa}) n First 2 (abA.{ E,aa}) = {E,aa} n {ab} = 0
;
Pour A, First 2 (Saa.{E,aa}) n First 2 (b.{E,aa}) = {ab,aa} n {b,ba} = 0.
Cette grammaire vérifie la CNS de grammaire LL(k) forte. Par conséquent, la
grammaire donnée est 11(2) forte. Cette propriété représente un intérêt pratique,
particulièrement pour les analyseurs basés sur les grammaires LL(k).

Définition 2.3 (condition nécessaire de grammaire LL(k))


Une grammaire G = (VN, VT, P, S) est dite LL(k) pour k ~ 1 fixé, si pour les
deux dérivations les plus à gauche {Leftmost Derivation)
(1) S ==>* 1m wAa ==> 1 mw~a ==>*wx
(2) S ==>* 1m wAa ==> 1mW'(a ==>*wy
tel que Firstk(x) = Firstk(y), on a nécessairement ~ = y.
Intuitivement, cela suppose qu'il n'y a qu'un seul chemin de S vers wx et wy
tel que Firstk(x) = Firstk(y), sinon si le symbole A produit ~ :;:. y alors cela
signifie qu'il y a plus d'un chemin pour aboutir à wx et wy sachant
que Firstk(x) = Firstk(y).
Analyse syntaxique 233

La grammaire G définie par les règles suivantes :


S~A 1 B

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

En outre, Firstk(x) = Firstk(Y) = {ak}. Cependant, la conclusion ~ = "(n'est pas


vraie. Par conséquent, la grammaire G ne peut pas être LL ( k) pour tout k ;::; 1.
On peut toutefois construire un automate à pile vide déterministe comme celui
de la Figure 119 qui reconnait le langage L(G) = {anObn 1 n;::; O} u
{anlb 2n 1 n ;::; O}.

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

Permettent une analyse gauche mais non une analyse droite.


Les séquences d'analyse suivantes des mots aaObb, aaOb, aalbbbb et albbb
montrent que l'automate à pile de la Figure 119 fonctionne de manière
descendante entièrement déterministe.
s, aaObb, #
q, aObb, #
q, Obb, ##
i, bb, ##
i, b, #
i, E, E
Le mot a été entièrement scanné et la pile est vide. Autrement dit, le mot aaObb a
été accepté.

a/#(##)
b/#(E)

0/#(#)

b/#(#)

Figure 119: Automate à pile déterministe modèle d'analyseur descendant


pour le langage L(G) = {anObn 1 n;::: O} u {anlb 2n 1 n ;::: O}

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

Figure 120 : Relations entre classes de grammaires à contexte libre

Par exemple, la Figure 120 indique que la sous-classe dénommée a correspond


à la famille des grammaires LR qui permettent une analyse gauche, mais qui ne
sont pas LL. De même, la sous-classe dénommée b correspond à la famille des
grammaires LR qui permettent une analyse droite, mais pas une analyse gauche,
etc.

2.2 Grammaires de précédence d'opérateurs ou GPO


Une grammaire G = (VN, VT, P, S) est d'opérateurs si:
elle est e-libre (pas de règle A ~ e) ;
elle ne possède pas de règle de la forme A~ aXY~ (pas de non-terminaux
adjacents ou contigus) ;

Les relations de précédence d'opérateurs sont dues à [Floyd, 1963]. La


précédence d'opérateurs correspond à ce qui est communément appelé priorité des
opérateurs. Il faut noter cependant, que les opérateurs dont il s'agit dans ce
236 Chapitre 6

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*}

Lastop(X) ={a E VT 1 X::::)+ ~ay, y E VN u {E} et~ EV*}


Par exemple :
E-7E+TIT
T-7 (E) 1 a
Firstop(E) = {+, (, a}
Lastop(E) = {+,),a}.
Ensuite, il s'agit de calculer les relations de précédence d'opérateurs comme
indiqué par les points suivants :
a= b si 3 A-+ aayb~ E P, y E VN u {E} et a,~ EV*.
a <:: b si 3 A-+ aaB~ E P et B ::::) + ybô, c'est-à-dire b E Firstop(B); a,~. ô E
V* ety E VN U {E}.
a::> b si 3 A-+ aBb~ E P et B ::::)+ ôay, c'est-à-dire a E Lastop(B); a,~. ô E
V*ety E VN U {E}.
Relations de précédence artificielles (cas pratique) :
$<::a si S ::::)+ yaô, c'est-à-dire a E Firstop(S); ô E V*ety E VN u {E}, S
l'axiome.
a::>$ si S ::::)+ ôay, c'est-à-dire a E Lastop(S); ô E V*ety E VN u {E}, S
l'axiome.
Soit à calculer ces relations pour la grammaire des expressions arithmétiques
simples parenthésées définie par les règles :
E-+E+TIT
T-+T*FIF
F-+ (E) 1 a

Comme préconisé, il s'agit de calculer d'abord les ensembles Firstop et


Lastop, ensuite il faut appliquer les points décrits ci-dessus pour calculer les
différentes relations de précédence d'opérateurs.

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

précédence d'opérateurs. Ces relations sont répertoriées dans la table de la


Figure 121.

Firstop Lastop
E a,+,*, ( a,+,*,)
T a, *, ( a,*,)
F a, ( a,)

Tableau XXXIII - Calcul des ensembles Firstop et Lastop

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
~ ~ ~ ~

$ <::;: <::;: <::;: <::;:

Figure 121 : Relations de précédence d'opérateurs

Définition 2.4 (Grammaire de précédence d'opérateurs)


Une grammaire est de précédence d'opérateurs (GPO)
si elle est d'opérateurs ;
si, en outre, elle possède au plus une relation de précédence d'opérateurs
pour toute paire (x, y) E VT u {$} x VT U {$}
Dans ce cas, la grammaire d'opérateurs qui a produit la table des relations de
précédence de la Figure 121 est une GPO puisqu'elle vérifie les deux conditions
précédentes.
Il existe d'autres sous-classes de grammaires de précédence comme les
grammaires de précédence simple (GPS), les grammaires de précédence étendue
(GPE), les grammaires de précédence faible, etc. Mais, comme mentionné déjà
auparavant, on se limite dans cet ouvrage aux grammaires de précédence
d'opérateurs (GPO) qui sont largement les plus utilisées, particulièrement, parce
que la quasi-totalité des langages comportent des sous grammaires d'opérateurs
qui engendrent des expressions arithmétiques et/ou logiques, relationnelles, etc.

2.3 Grammaires LR(k)


L'appellation LR (k) vient de l'abréviation de left et right (gauche et droite).
L (left), c'est pour la lecture de gauche à droite « left to right parsing ». Quant à
R (right ), « right analysis », c'est pour rappeler que l'analyse a un rapport avec
238 Chapitre 6

la dérivation droite. Enfin, le symbole k ;::: 0, correspond au nombre de caractères


en prévision (k-lookahead). La notion de grammaire LR est due à [Knuth, 1965].
Soit G = (VN, VT, P, S) une grammaire à contexte libre.
*rm rm
Si S ~ aAw ~ a{Jw est une dérivation droite, y est dit préfixe actif (viable) pour
G s'il est préfixe de a{3.
[A~~1.~2, u] est un item LR(k) si A~ ~1~2 E P et u E v;k , c'est-à-dire lui::; k
•rm rm
[A~~1.~2, u] est valide pour a~1, si 3 S ~ aAw ~ a{3 1 {3 2 w tel que u E Firstk(w)

Définition 2.5 (Grammaire augmentée)


G' = (VN u {S'}, VT, P', S') est une grammaire augmentée de
G = (VN, VT, P, S) telle que P' =Pu {S' ~ S}.

Définition 2.6 (First Epsilon-libre)


EFFk(a) ou Epsilon Free First d'ordre k d'une chaine a : (les X-premiers
symboles de v.;k (lxl ::;; k), e-libres) est définie par:
(1) si a ne commence pas par XE VN alors EFFk(a) = Firstk (a);
(2) sinon EFFk(a) = {w 1 3 a.;; f3 ~ wx où f3 *
Awx, \::f A E VN et w =
Firstk ( wx)}. Dans ce cas, il ne faut pas remplacer le non-terminal A pare
(si 3 A~ e).

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}

CNS de grammaire LR(k), k;::: 0 (Théorème)


G = (VN, VT, P, S) est une grammaire LR(k), si et seulement si la condition
suivante est satisfaite \::f u E v.;k.
Soit a~ un préfixe actif de a~w de la grammaire augmentée G'.
Si [A~~., u] est valide pour a~, alors il ne peut exister simultanément
[B ~~1.~2, v] qui soit valide pour a~ avec u E EFFk (~2 v). Il faut noter que ~2
peut être = e.
Pour montrer qu'une grammaire est LR(k), il faut calculer la collection des
ensembles des items LR (k). C'est le but de la procédure suivante :
Analyse syntaxique 239

Procédure de calcul des ensembles Vk(Y) d'items LR(k) :


1°/ Construction de l'ensemble initial des items Vk(E) ;
initialement Vk(E) = r/J.
(a) Si S ~ o alors ajouter [S~.o, E] dans Vk(E); S est l'axiome;
(b) Si [A~.Ba, u] EVk(E) et B ~ ~EP alors ajouter dans Vk(E), l'item [B~.~, x]
pour tout x E Firstk(au) ;
(c) répéter (b) jusqu'à ce qu'il ne soit plus possible d'ajouter de nouveaux items
dans Vk(E).
2° / Vk(X 11 X2 ,
Xi_ 1 ) est supposé calculé avec i
••• ~ n, on construit alors l'ensemble
successeur Vk(X 1 , X2 , ... Xi) comme suit :
(a) Si [A~a. X 1 ~, u] E Vk(X 11 X2 , ••. Xi_ 1 ) ajouter [A~a X 1 .~, u] dans
Vk(X 11 Xz, ... Xi);
(b) Si [A~a.B~, u] E Vk(X 11 X2 , ... Xi) et B ~ o E P alors ajouter [B~.o, x]
dans Vk(X 11 X2 , ••• Xi) V x E Firstk(~u);
(c) répéter (b) jusqu'à ce qu'il ne soit plus possible d'ajouter de nouveaux items
dans Vk(X 11 Xz, ... Xi)·
Remarque 2.3
Le point (a) de la partie 2° de la procédure ci-dessus peut être remplacé par une
écriture plus condensée comme suit : si @i = Vk(Y) alors GOTO(@i, X) = @i+l =
Vk(yX); X E VTuVN.
Par exemple, soit à vérifier si la grammaire G définie par les deux règles de
production S ~ SaSb 1 E est LR(l).
La grammaire augmentée correspondante est définie par les règles :
S' ~ S
S ~ SaSb 1 E.
On peut vérifier en parallèle la condition LR(l), c'est-à-dire au fur et à mesure
que l'on calcule les ensembles des items. Ainsi, si l'un des ensembles calculés
s'avère inconsistant (contradictoire), il sera inutile de poursuivre le processus de
calcul, car la grammaire n'est pas LR(l). Si au contraire, tous les ensembles
d'items sont consistants, on conclut évidemment que la grammaire est LR(l).

@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

[S~S.aSb, a] EFF1 (aSblaSba) = {a}


et on a e <t. {a}, donc pas de conflit
@ 2 = V1 (Sa) = GOTO (@ 11 a)
[S~Sa.Sb, E]
[S~Sa.Sb, a]
[S~.SaSb, b)
[S~., b] EFF1 (SblSbalSaSbblSaSba) = 0
et on a a et b <t. 0, donc pas de conflit
[S~.SaSb, a]
[S~ .• a]
@3 = Vi (SaS) = GOTO (@ 2 , S)
[S~SaS.b, E]
[S~SaS.b, a] EFF 1 (bjbalaSbblaSb) =
[S~S.aSb, b) {a, b},mais il n'y a pas d'item de la
[S~S.aSb, a] forme [A ---+p., u] tel que u E {a, b}.

@ 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

Il n'y a pas de nouveaux ensembles d'items à calculer; donc on s'arrête. De plus,


le fait qu'il n'y a pas de conflit (pas d'ensemble d'items, contradictoire), signifie
sans conteste, que la grammaire est LR(l).
Analyse syntaxique 241

A l'issue de la description de tous ces concepts et formalismes indiquant


quelles sont les procédures à suivre pour vérifier qu'une grammaire est
déterministe (au sens LL, LR ou de précédence, permettant ainsi une analyse
déterministe), il devient alors aisé d'aborder les analyseurs déterministes comme
prévu. On décrit ci-après, quelques méthodes et techniques parmi les plus
largement utilisées dans le domaine.

3 Quelques méthodes d'analyse syntaxique déterministe


On se limitera à n'esquisser brièvement qu'une infime partie, mais claire et
concise, sur les analyseurs déterministes. On se focalisera particulièrement sur :
Les analyseurs descendants LL(l) ; on parlera à ce propos de la descente
récursive prédictive, ainsi que de la technique de la table d'analyse prédictive
LL(l).
Les analyseurs ascendants dont on donnera un bref aperçu sur la méthode de
précédence d'opérateurs, ensuite on s'intéressera à la classe la plus large de la
famille des analyseurs déterministes, à savoir, les analyseurs fondés sur les
grammaires de la famille LR(k).

3.1 Analyseurs LL(l)


Avant tout, on suppose qu'on dispose d'une grammaire LL(l).
Analyseur basé sur la descente récursive.
Les caractéristiques de ce type d'approche sont bien connues et elles se résument
comme suit:
L'analyseur est descendant déterministe (donc sans retour arrière) ;
L'analyseur est fondé sur des appels récursifs selon le sens des dérivations. De
ce fait, la localisation des erreurs a toujours lieu à l'endroit approprié ;
La lecture et l'analyse vont dans le même sens (en descendant, de gauche à
droite) ;
La construction de l'arbre d'analyse se fait selon un parcours en profondeur ;
La pré lecture (prévision) d'un symbole à l'avance (k = 1, d'où le nom LL(l)) ;
La technique est de nature lente à cause, particulièrement, des multiples
appels récursifs qui augmentent inévitablement la complexité temporelle de
l'analyseur ;
La technique de la descente récursive n'utilise aucune structure de données
particulière, donc ne nécessite pas d'espace mémoire supplémentaire (à part
celui pris par l'algorithme d'analyse) ;
Tout comme les parcours avec les diagrammes de transition vus en analyse
lexicale, la descente récursive constitue un analyseur ad-hoc (particulier) qui
n'est applicable qu'à la grammaire LL(l) sur laquelle il s'appuie. Par
conséquent, il faudrait en écrire un autre, en cas de changement de
grammaire;
242 Chapitre 6

Très facile à écrire et à implémenter ;


Malgré son aspect intuitif, la descente récursive demeure très utilisée dans de
nombreux interpréteurs, particulièrement pour sa clarté dans les différents
traitements. Ces derniers sont généralement guidés par la syntaxe du langage
et les règles de production de la grammaire sur laquelle est bâtie la descente
récursive.

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.

Définition 3.1 (Descente récursive)


Etant donnée G = (VN, VT, P, S) une grammaire LL(l).
On associe à chaque X E VN une procédure qui peut être récursive. La
descente récursive est constituée d'un réseau de procédures qui peuvent
s'appeler mutuellement et récursivement.
La descente récursive définit un analyseur prédictif où le symbole de
prévision (ou pré lecture} détermine de manière non ambiguë, la procédure à
choisir pour chaque non-terminal (élément de VN)·
Etant donnée une grammaire G. Une descente récursive correspond
schématiquement à un algorithme de parcours de graphe. Ce parcours peut avoir
lieu sur le diagramme de transition d'un RAF (réseau d'automates d'états finis)
issu de la grammaire G ou sur le diagramme de transition associé à G ou encore
sur le graphe syntaxique de G. Les notions sur les diagrammes ou les graphes
associés aux grammaires ont été étudiés intensivement au chapitre 3.
Les règles d'application d'une descente récursive sont les suivantes :
L'analyseur syntaxique commence par un appel à l'axiome avec un symbole en
prévision (ce symbole anticipé dénote une unité lexicale renvoyée par
l'analyseur lexical).
On utilise la production A ~ a. si le symbole en prévision appartient à
l'ensemble First1 (a.).
On se rabat par défaut sur la règle A ~ e (lorsqu'elle existe), si le symbole en
question n'appartient pas à First 1 ( a.).
Si a. = xp, X E VN, la procédure en cours associée au non-terminal A (avec
A~ a.), effectue un appel à la procédure X.
Si, au contraire, a = ap avec le symbole a E VT et si le symbole a coïncide
avec le symbole en prévision, alors on procède à la lecture d'un nouveau
symbole en prévision d'un éventuel prochain appel.
Si à un point quelconque de l'algorithme de la descente récursive, on a le
symbole lu a qui est -: /:- (différent) du symbole prévu, une procédure de
traitement de l'erreur sera systématiquement appelée.
Analyse syntaxique 243

Pour une bonne organisation et un bon déroulement de la descente récursive,


c'est-à-dire la synchronisation de la lecture et de l'analyse des symboles du flux
d'entrée, on écrit au préalable une procédure que l'on nomme Accept qui a pour
finalité de :
vérifier si un symbole courant est conforme à celui attendu (prévu) ;
fournir le symbole suivant.
Ces deux points sont schématiquement résumés dans la procédure Accept
suivante:
Procédure Accept ( T: lexitem) ;
début si symbole = T
alors symbole {-- symbole_ suivant
sinon erreur ( )
fsi
fin;
Afin de mettre en œuvre le principe de construction et de fonctionnement
d'une descente récursive, on s'appuie sur la grammaire définie par les règles de
production suivantes :
E --7 TM (l)
T --7 FN (2)
M --7 +TM (3) I e (4)
N --7 * FN (5 ) I e (5 )
F --7 a (7) 1 (E) (B)
La descente récursive associée à ces règles de production est représentée par les
procédures E, T, M, Net F suivantes :

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 ;

Comme on peut le voir, il y a autant de procédures qui composent la descente


récursive qu'il y a de non-terminaux dans la grammaire. Ce réseau de procédures
offre une vue générique conforme à l'agencement des règles de production de la
grammaire ayant servi à construire la descente récursive.
Pour enrichir la descente récursive et garder la trace du déroulement de
l'analyse, à savoir, la dérivation canonique gauche, il convient d'imprimer les
numéros des règles de production utilisées au cours du processus d'analyse. Pour
plus d'information à propos de la dérivation canonique, le lecteur est invité à
consulter le chapitre 1 où l'on donne certains rappels sur la théorie des langages et
beaucoup de détails concernant la notion de dérivation canonique,
particulièrement.
La descente récursive est une manière d'exprimer intuitivement
(algorithmiquement) les dérivations (règles de production utilisées au cours de
l'analyse). Dans ce qui suit, on reconsidère la descente récursive composée des
procédures précédentes, auxquelles on adjoint les numéros des règles de
production aux endroits appropriés.

Procédure E;
début
Analyse syntaxique 245

écrire {1) ;

M '
fin;

Procédure T ;
début
écrire {2) ;

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

Analyseur basé sur la table prédictive LL(l)


L'analyseur fondé sur la table prédictive effectue la même tâche que la descente
récursive, mais selon une approche différente.

E => TM => FNM => aNM => aM => a E


(1) (2) (7) (4) (6)
/;..
\
\

n,=12746 ''
''
''
.li>t9f. . . ' . . '
Otq. ......... - - -
~ M
T

F
N
1 1
a e

Figure 122 : Arbre d'analyse du mot "a" par la descente récursive


Les principales caractéristiques qui le distinguent de la descente récursive sont,
entre autres :
L'espace alloué pour la table prédictive et la pile.
La récursivité de la grammaire (appels) est directement gérée par la pile de
l'analyseur.
Comparativement à la descente récursive, l'approche tabulaire est à priori plus
générale puisque la méthode de construction de la table prédictive (une fois
automatisée), est réutilisable avec n'importe quelle grammaire LL(l). Il s'agit
donc, tout simplement, de prévoir un introducteur de la grammaire pour
laquelle on voudrait construire un analyseur prédictif LL( 1).
L'analyseur est plus performant, puisqu'il accède directement à la table où se
trouve l'action indiquant le traitement à effectuer, contrairement à la descente
récursive qui elle, perd énormément de temps dans les appels et retours entre
les différentes procédures qui constituent l'analyseur.
Par ailleurs, quand la table occupe énormément d'espace inutilement, on fait
appel à certaines techniques de compression. On peut par exemple utiliser une
structure de données dynamique comme la liste chainée, mais ça sera au
détriment de la rapidité de l'analyseur.
La construction de la table prédictive LL(l) est résumée dans les points de la
procédure suivante :
248 Chapitre 6

Procédure Table_ predictive ;


Pour chaque production A ~ a
Pour tout a E First1 (a) et a -:t: e
Ajouter A ~ a dans la case M [A, a] ;
si e E First 1 (a)
alors Pour tout b E Follow1 (A)
Ajouter A~ a dans la case M [A, b] ;
Tout autre case non remplie de la table M correspond à une erreur ;
Pour une représentation plus concise, au lieu de A ~ a, on met juste a dans
la case concernée de la table M. Pour plus d'information on ajoute le numéro (i)
de la règle de production A ~ a. On obtient ainsi la table prédictive illustrée par
le Tableau XXXIV.

a + * $
E TM, TM,
T
M +TM, (3) 4 e, 4
N e, 6 *FN, (5) 6 e, 6
F a, 7

Tableau XXXIV - Table prédictive de la grammaire LL{1) des expressions


arithmétiques simples parenthésées
Cette table est utilisée par un analyseur spécial qui doit interpréter le contenu
de chacune de ses cases. L'analyseur peut passer par plusieurs états qui sont
matérialisés chacun par une configuration précise.
La configuration générale est un triplé (p, w, 1t1) constitué :
du contenu p de la pile (la pile sert à stocker les résultats intermédiaires de
l'analyse syntaxique) ;
la chaine courante d'entrée w ;
la sortie 1t1: numéros des règles déjà utilisées
Le fond de la pile est représenté par un symbole spécial, par exemple $
n'appartenant pas à VT.
Pour marquer la fin de l'analyse, on peut utiliser également le même symbole$
qui repère la fin de la chaine à analyser.
La configuration initiale est représentée par le triplet (Axiome$, w$, e). On
dirige, par convention, le sommet de pile vers la gauche conformément à l'analyse
gauche.
L'analyseur consiste à lire une chaine d'entrée, et à l'analyser en s'appuyant
sur ce que prédit la table. Le couple (sommet de pile, caractère lu) indique,
Analyse syntaxique 249

conformément à l'information recueillie sur la table prédictive, quelle est l'action à


appliquer. Plusieurs cas peuvent se présenter, dont :
M [A, a] = a., (i) qui stipule qu'il faut :
• empiler (a.) ;
• émettre le numéro de règle (i) en sortie ;
M (sommet, caractère courant) = M (a, a) indique une coïncidence signifiant
qu'il faut :
• dépiler (a) ;
• lire le prochain symbole ;
M [$, $] =la chaine a été entièrement analysée avec succès.
M [] =case vide indique un cas d'erreur ;
Soit à analyser l'expression "a* a" par la méthode de la table prédictive LL (1). Il
s'agit de suivre scrupuleusement les actions indiquées par la table prédictive du
Tableau XXXIV.
E$, a *a$, E configuration initiale
TM$, a *a$, 1 dérivation règle 1
FNM$, a *a$, 2 dérivation règle 2
aNM$, a *a$, 7 dérivation règle 7
NM$, *a$, E coïncidence ; lire caractère suivant
*FNM$, *a$, 5 dérivation règle 5
FNM$, a$, E coïncidence ; lire caractère suivant
aNM$, a$, 7 dérivation règle 7
NM$, $, E coïncidence ; lire caractère suivant
M$, $, 6 dérivation règle 6
$, $, 4 dérivation règle 4
$ $ E « Succès »
La dérivation canonique 1t1 = 1 2 7 5 7 6 4 représente la trace de l'analyse
précédente de la chaine 11 a * a".
L'analyse de la même chaine avec la descente récursive aurait évidemment
produit les mêmes numéros de règles, mais à la différence, avec la descente
récursive, le processus aurait été beaucoup plus lent et onéreux, à cause,
particulièrement, des appels récursifs dont la lenteur n'est plus à démontrer.
Avec un automate à pile déterministe, cette récursivité est complètement
neutralisée, grâce notamment, à l'utilisation de la pile qui fait partie du dispositif
de l'automate. Par ailleurs, l'automate peut être utilisé en s'appuyant sur son
diagramme de transition, ce qui accélère le processus d'analyse. Pour plus
d'information sur l'automate à pile, se référer au chapitre 3.
On peut donc utiliser le diagramme de transition de l'automate à pile déterministe
qui reconnait les expressions arithmétiques engendrées par la grammaire de
l'exemple précédent. Ce diagramme est donné par la Figure 123.
250 Chapitre 6

a/ # (#)

$ / # (&) *

État final

)I# (r.)

Figure 123: Diagramme de transition de l'automate à pile déterministe


modèle d'analyseur qui reconnait les expressions arithmétiques simple
parenthésées

Pour pouvoir comparer la technique de la table prédictive avec celle de


l'automate à pile, on doit analyser la même expression, à savoir, "a* a" suivie du
marqueur de fin d'analyse noté $. On utilise dans ce cas un automate à pile à
critère d'acceptation généralisé (par état final et pile vide). L'état final de
l'automate est noté par la lettre f dans la Figure 123. On trouvera de plus amples
détails sur l'automate à pile dans le chapitre 3.
Soit alors à analyser l'expression "a * a" par l'automate à pile précédent :
o, a* a$, # 1-
1, *a$, # 1-
0, a$, # 1-
1, $, # 1-
f, e e Stop

L'automate a atteint son état final d'acceptation « Succès » en moins de steps


(pas) que l'analyseur fondé sur la table prédictive LL(l). On obtient 11 steps avec
la méthode de la table prédictive pour 4 steps avec l'automate à pile déterministe
équivalent.

3.2 Analyseurs ascendants déterministes basés sur la précédence


d'opérateurs
On utilise la table des relations de précédence d'opérateurs, issue d'une grammaire
de précédence d'opérateur (GPO).
L'algorithme s'appuie uniquement sur les priorités (précédences) définies par la
table de précédence d'opérateurs, comme celle définie plus haut.
La configuration générale de l'analyseur fondé sur les GPO est définie par le
quadruplet : Pile, Chaine, Relation, Action
Dans Pile (la pile) sont recueillis les résultats intermédiaires de l'analyse ;
Analyse syntaxique 251

Chaine représente la chaine courante ou restante non encore analysée ;


Relation est, soit <, soit =, soit > ;
Action est, soit un décalage, soit une réduction.

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

fait ; / * x est le terminal le plus récemment dépilé */


Soit alors à analyser l'expression "a * (a + a)", comme suit :
$ a < shift (décaler)
$a * > reduce (réduire)
$ * < shift
$* ( < shift
$*( a < shift
$*(a + > reduce
$*( + < shift
$*(+ a < shift
$*(+a > reduce
252 Chapitre 6

$*(+ ) )> reduce


$*( ) - shift
$*( ) $ )> reduce
$*( $
$* $ )> reduce
$ $ «Succès» (chaine acceptée)
On se propose à présent d'analyser la même chaine "a * (a+ a)", mais en
utilisant l'option de la grammaire squelette. La grammaire squelette est ambiguë,
mais peut tout de même être utilisée malgré son ambigüité, dans un analyseur
déterministe. Elle n'est utilisée que pour l'avantage qu'elle offre, à savoir, la
rapidité du processus d'analyse. Le problème de l'ambigüité est contourné en
mettant de l'avant la précédence (priorité) d'opérateurs qui évacue totalement
toute forme de non déterminisme.
La grammaire squelette comporte moins de règles que son homologue définie
par l'ensemble des règles :
s~s+BIB
B~B*CIC
C ~ (S) 1 a
En effet, elle est définie par les règles suivantes :
s ~ s + s {l)
s ~ s * s (2)
S ~ (S) (3)
S ~a (4 )
Elle ne comporte qu'un symbole non-terminal S, ce qui lui confère une plus
grande rapidité d'analyse. On peut observer cela dans le déroulement de l'analyse
de la chaine "a* (a+ a)".

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

$S*(S + (<+ shift


$S*(S+ a +<a shift
$S*(S+a ) a::>) reduce règle 4
$S*(S+S ) + ::> ) reduce règle 1
$S*(S ) (=) shift
$S*(S) $ ) ::> $ reduce règle 3
$S*S $ * ::> $ reduce règle 2
$8 $ «Succès» 1tr=23144
Il y a juste un pas de moins par rapport à l'option qui utilise l'algorithme au
lieu de la grammaire. 13 steps au lieu de 14, mais, ceci n'est pas très significatif.
Les différences notables, en revanche, résident dans le fait qu'avec la grammaire, il
est toujours possible de différer le processus de traduction, si on devrait traduire
l'expression analysée. D'autre part, la grammaire se prête particulièrement mieux
pour créer facilement un schéma de traduction dirigée par la syntaxe (STDS). On
a déjà vu dans le chapitre 4 comment associer un schéma de traduction ou des
règles sémantiques pour les règles de production.

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

Tableau XXXV- Ensembles Firstop et Lastop associés à la grammaire des


expressions arithmétiques simples non parenthésées

a + * $
a )> )> )>

+ <! )> <! )>

* <! )> )> )>


$ <! <! <!

Tableau XXXVI- Table des relations de précédence d'opérateurs issue de


la grammaire des expressions arithmétiques simples non parenthésées

Pour calculer les priorités (précédences) numériques (priorités pondérées), on


peut adopter deux approches :
approche algorithmique (intuitive) ;

recherche du chemin le plus long dans un graphe.


Dans les deux cas, on tient compte de l'associativité des opérateurs (voir la
table des relations de précédence d'opérateurs ci-dessus).

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

On peut analyser une expression arithmétique de la même manière qu'avec la


table des relations de précédence d'opérateur classique. Soit alors à tester
l'expression "a + a * a".
On utilise, comme d'habitude, le symbole $, comme marqueur de fin
d'expression et également comme fond de pile. On aura ainsi la séquence d'analyse
suivante:
$ a f($) < g(a) shift
$a + f( a) > g(+) reduce 3
$E + f($) < g(a) shift
$E+ a f(+) < g(a) shift
$E +a * f( a) > g(*) reduce 3
$E + E * f(+) < g(*) shift
$E + E * a f(*) < g(a) shift
$E + E *a $ f( a) > g($) reduce 3
$E+E*E $ f(*) > g($) reduce 2
$E+E $ f(+) > g($) reduce 1
256 Chapitre 6

$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.

Approche du chemin le plus long dans un graphe


On utilise un graphe biparti comme suit :

a <:: b alors on a la configuration graphique


ci-contre :

a ::> b alors on a la configuration graphique


ci-contre :

a = b alors a et b appartiennent au même groupe, c'est-à-dire qu'il n'y a pas


d'arc qui va du sommet a au sommet b. Mais, puisqu'il s'agit d'un graphe biparti
on prend en considération cette particularité dans le calcul des priorités des
sommets a et b quand ils se trouvent respectivement à gauche (dans la pile) et à
droite (dans la sous-chaine en cours d'analyse). Le cas des parenthèses illustre
clairement cette situation (quand a = b). Les valeurs des priorités f( a) et g( b)
calculées, des opérateurs (sommets du graphe) a et b qui peuvent être dans ce cas
de figure, sont données par le petit Tableau XXXVIII.

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

On voit très bien que !( 11 ( 11 ) = 0 et que g( 11 ) 11 ) = 0, c'est-à-dire que !( 11 ( 11 ) =


g( 11 ) 11 ) =O. On constate aussi que !( 11 ( 11 ) < g("(") et que!(")") > g(")"). Mais, il
n'y a aucune relation (cas d'erreur) entre la parenthèse fermante ")" quand elle
est à gauche (dans la pile) et la parenthèse ouvrante " ( 11 quand elle est à droite
(dans la sous-chaine en cours d'analyse), même si on a!(")") = 6 et g( 11 (") = 5 ;
ce qui peut facilement induire en erreur. Pour éviter ce piège, il faut revenir à la
table des relations d'opérateurs classique et remarquer en fait qu'il n'existe pas de
relation entre la parenthèse fermante (quand elle est à gauche) et la parenthèse
ouvrante (quand elle apparait à droite). Mais, pour récolter tous les cas d'erreurs,
il faut, d'une part, considérer les cases vides (excepté la relation ($, $) qui n'est
pas un cas d'erreur) de la table des relations, d'autre part, il faut prendre en
compte aussi les cas d'erreurs cachées comme par exemple **, ou ++, ou *+, ou
Analyse syntaxique 257

+*, 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

Figure 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
Par exemple, en partant du sommet a appartenant à la séquence de nœuds de
la partie gauche du graphe, le chemin le plus long est (a, *, +, +, $), c'est-à-dire
de longueur = 4. Le résultat final est obtenu en agissant de la sorte, c'est-à-dire
en cherchant à mettre en évidence le chemin le plus long à partir de chaque
sommet du graphe. La table du Tableau XXXIX donne toutes les priorités (à
gauche ou à droite). Ce résultat confirme celui trouvé avec la méthode intuitive
258 Chapitre 6

(algorithmique), précédemment. A titre d'exercice, le lecteur peut chercher ces


chemins manuellement.

a~~~
+ 2 1
* 4 3
$ 0 0

Tableau 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
Mais, afin de calculer automatiquement les priorités, comme préconisé, on peut
utiliser la table des relations (matrice d'adjacence du graphe) sous la forme codée
suivante:
-1 pour le codage de la relation inférieure 1 <1
1 pour le codage de la relation supérieure '>'
0 pour le codage de la relation d'égalité '=' ; ici on n'a pas ce cas, car les
parenthèses ne sont pas prévues dans la grammaire.
On laisse, à titre d'exercice pratique, l'implémentation sur machine du graphe
par cette matrice, et son utilisation pour trouver le chemin le plus long à partir de
chaque sommet x du graphe. Pour rappel, le chemin le plus long représente la
priorité (gauche f(x) ou droite g(x) correspondant respectivement à leftprec [x] et
rightprec [x]).
La matrice du Tableau XL illustre la représentation matricielle du graphe de
la Figure 124.

a + * $
a 1 1 1
+ -1 1 -1 1
* -1 1 1 1
$ -1 -1 -1

Tableau XL - Matrice représentant les relations de précédence des


opérateurs de la grammaire des expressions arithmétiques simples sans
parenthèses

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

3.3 Analyseurs ascendants déterministes basés sur des grammaires


LR (k) avec k ~O
On a déjà défini préalablement quels sont les critères requis pour qu'une
grammaire soit LR (k). L'objectif ici est de montrer comment construire un
analyseur LR (k). En d'autres termes, il s'agit, d'abord de décrire comment on
construit la table LR (k), ensuite de préciser comment utiliser celle-ci dans un
algorithme d'analyse LR (k).
Les analyseurs ascendants, quels qu'ils soient, sont tous basés sur le principe
décaler/réduire (shift/reduce).
Les analyseurs LR (left to right), SLR (Simple LR) et LALR (lookahed LR),
utilisent le même algorithme d'analyse. Cependant, leurs tables d'analyse
présentent quelques différences. A titre indicatif, si la table LR d'un
compilateur présente plusieurs milliers de lignes, son homologue, pour une
grammaire SLR, n'en présente que quelques centaines. Techniquement, la
construction de la table SLR est quasiment identique à celle de la table LR.
La procédure de construction de la table d'analyse LR associée à une
grammaire LR est la suivante :
Entrée : Une grammaire augmentée G'
Sortie : Table d'analyse LR
(1) Construction de la collection des ensembles d'items LR (1), C = {@a, @i ... ,@n}
pour la grammaire augmentée G'.
(2) On assimile chaque ensemble d'items @i à un état de numéro i.
(3) La table est divisée en deux parties comme suit :
Action e {décaler, réduire, accepter, erreur}.
Transition : GOTO (@i, X) = @f
a) Si [A~a..a~, b]e@ii alors Action [i, a] = Dj et j est tel que GOTO (@i, a)
= @i- Ici le symbole a eVT.
b) Si [A~a.., a] est dans @i et A -:F- S', alors Action [i, a] = Rj tel que :
A ~ a. (J). Ici j correspond au numéro de règle de production utilisée pour
la réduction Rj.
c) Si [S'~S., E] e@i, alors Action [i, $] = « Accepter » est la situation LR
d'acceptation.
d) Si GOTO (@i, A) = @j alors on a Successeur [i, A] = j.
e) Les entrées non définies sont des cas d'erreurs.
f) L'état initial de l'analyseur est celui contenant [S' ~ .S, $].

Afin d'assimiler les différentes phases de l'algorithme de construction ci-dessus,


il convient de s'appuyer sur un exemple concret.

Ainsi, soit à construire la table d'analyse LR pour la grammaire augmentée


définie par P' = {S' ~ S (o), S ~ SaSb (l) 1 E (2l}.

La collection canonique LR(l) que l'on note C a déjà été construite


précédemment en section 2.3 de ce chapitre. Cette collection notée
260 Chapitre 6

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

Tableau XLI - Table d'analyse LR pour la grammaire


S' ~ S (o); S ~ SaSb {l) 1 e <2l

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].

Grammaire SLR (k) k ~ 0 (Théorème)


On construit au préalable C ={@o, @1, ... @n} la collection canonique LR(O)
pour la grammaire G qui n'est pas forcément LR(O).
Soient [A~a..p] et [B~y.ô] deux items distincts dans un ensemble quelconque
@i.
Analyse syntaxique 261

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

et on voudrait vérifier si elle est SLR {O).


On augmente G bien que ça ne soit pas vraiment nécessaire (puisque ici S
n'apparait pas à droite des autres productions). On obtient donc les règles :
S' ~ S
s~c 1 D
c ~ac 1 b
D~aDlc

De plus, comme prévu ci-dessus, on va adopter la notation [A~a..~] au lieu de


[A~a..~, e]. On peut même supprimer les crochets si on le désire, et écrire A~a..~
au lieu de [A~a..~]. Ensuite, pour anticiper, on vérifie la condition SLR {O), au
fur et à mesure que l'on avance dans le calcul de la collection canonique des
ensembles d'items LR {O).
On présente ci-après, les différentes étapes de calcul de la collection des
ensembles d'items LR {O). On vérifie également à chaque étape si l'ensemble
d'items @i calculé n'est pas contradictoire {ne présente aucun conflit en
application de la condition SLR {O)).
@o = s·~.s
s~.c
S~.D
c~.ac
c~.b
D~.aD
D~.c

La condition (1) du théorème est satisfaite, donc pas de conflit.


@1 = GOTO (@o, S) = s·~s.
La condition (4) est satisfaite, de plus il y a qu'un seul item, et il ne peut pas
évidemment rentrer en conflit avec lui-même, donc pas de contradiction.
@2 = GOTO {@o, C) = S ~ C.
262 Chapitre 6

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.

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.
@s = GOTO (@4, D) = D~aD. 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.
GOTO (@ 4 , a) = @4 déjà calculé.
GOTO (@ 4 , b) = @ 5 déjà calculé.
GOTO (@ 4 , c) = @5 déjà calculé.
Les états @i, @2, @3, @5, @1 et @s sont tous des états finals, donc on s'arrête,
puisque à partir de l'état @ 4 , on ne peut transiter que vers des états déjà existants
@ 4 , @ 5 et @ 6, donc qui se répètent. On déduit que la grammaire est SLR(O),
puisqu'il n'y a plus de nouveaux ensembles à calculer et que tous les ensembles
d'items calculés ne présentent aucun conflit.

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

L'algorithme de construction de la table SLR associée à une grammaire SLR


est donné comme suit :
Entrée: Une grammaire augmentée G'.
Sortie: Table d'analyse SLR
1) Construction de la collection des ensembles d'items LR (0), C = {@o,@1 ... @n}
pour la grammaire augmentée G'.
2) On assimile chaque ensemble d'items @i à un état de numéro i.
3) La table est divisée en deux parties comme suit :
Action E {décaler, réduire, accepter, erreur}.
Transition : GOTO (@i, X) = @i-
Si [A~a.a~, b] E @i, alors Action [i, a] = Dj et j est tel que GOTO (@i, a)
= @1. Ici le symbole a E VT.
a) Si [A~a.] est dans @i et A* S', alors Action [i, a] = Rj tel que: A~a (J)
pour tout symbole a E Follow1 (A). Ici j correspond au numéro de règle de
production utilisée pour la réduction Rj.
b) Si (S'~S.) E @i alors Action (i, $) = «Accepter» est la situation SLR
d'acceptation.
c) Si GOTO (@i, A) = @1 alors on a Successeur [i, A) = j.
d) Les entrées non définies sont des cas d'erreurs.
e) L'état initial de l'analyseur est celui contenant [S'~.SJ.

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.

Soit la grammaire G définie par les règles suivantes :


S~L=R
S~R
264 Chapitre 6

L~ *R
L~a
R~L

LR (1)

LALR (1)
r
SLR (1)

Figure 125: Hiérarchie des grammaires LR (LR, SLR, LALR)

En construisant la collection canonique LR (0) pour la grammaire augmentée


G', on peut vérifier certaines conditions parallèlement, sans qu'il soit nécessaire
d'aller jusqu'au bout de cette construction.
@o = S' ~ .S
S ~ .L = R
s~.R
L~.*R
L~ .a
R~.L}

@i = GOTO (@o, S)
@i = s·~s.

@2 = GOTO (@o, L)
@2= S ~ L. = R
R~L.

G n'est ni LR (0), ni SLR (0). En effet, dans l'ensemble @ 2 , il y a un conflit


décalage/réduction, car en appliquant le théorème de grammaire SLR (k) on
obtient Follow 0 (R) = {e} et EFF0 (=R) = {e}, c'est-à-dire Follow 0 (R) n EFF0
(= R)* 0.
Est-elle cependant SLR ( 1) ?
S ::::::> L = R ::::::> *R =R, donc le symbole '=' E Follow1 (R). En considérant
*
l'ensemble @2, on constate que EFF1 ( = R. Follow1 (S)) n Follow1 (R) 0, c'est-à-
dire que la grammaire n'est pas SLR (1). L'analyseur SLR n'est pas suffisamment
puissant pour décider de l'action que doit effectuer l'analyseur (décalage ou bien
Analyse syntaxique 265

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

Pas de conflit, donc ensemble non contradictoire ;


@i = GOTO (@o, S) = s·~s.
@2 = GOTO (@o, C)
@2 = s~c.c
c~.cc
c~.

Pas de conflit, donc ensemble non contradictoire ;


@3 = GOTO {@o, c)
@3 = C~c.C
c~.cc
c~.d

Pas de conflit, donc ensemble non contradictoire ;


266 Chapitre 6

@4 = GOTO (@o, d) = C~d.


Pas de conflit, donc ensemble non contradictoire ;
@5 = GOTO (@2, C) = s~cc.
Pas de conflit, donc ensemble non contradictoire
GOTO (@2, c) = @3 déjà calculé
GOTO (@ 2, d) = @4 déjà calculé
@5 = GOTO (@3, C) = C~cc.
Pas de conflit, donc ensemble non contradictoire
GOTO (@3, c)= @3, déjà calculé ;
GOT0(@3, d) = @4, déjà calculé.

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

@5 = GOTO (@2, C) = {[s~cc., E)}

L'ensemble @5 possède un seul item, donc pas de conflit ;


@6 = GOTO (@ 2 , c) = {[C~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 @ 6 est non contradictoire. De plus, il ne diffère de l'ensemble @ 3 que
par la deuxième composante des items. Autrement dit, les items de @3 et @5
comportent respectivement les mêmes cœurs.
@1 = GOTO (@2, d) = {[C~d., e)}
L'ensemble @ 7 possède un seul item, donc pas de conflit. Par ailleurs, il ne diffère
de l'ensemble @ 1 que par la seconde composante de son item. Autrement dit,
l'unique item de @ 1 , ainsi que celui de @1 comportent le même cœur.
@s = GOTO (@3, C) = {[C~cC., c 1 d)}
L'ensemble @ 8 contient deux items de réduction qui ne sont pas en conflit, car c
d. Donc, l'ensemble n'est pas contradictoire ;
-=t.

GOTO = @3 , a déjà été calculé, et il ne comporte pas de conflit;


(@ 3 , c)
GOTO (@ 3 , d) = @ 4, a déjà été calculé, et il ne comporte pas de conflit;
GOTO (@ 6 , c) = @ 6 , a déjà été calculé, et il ne comporte pas de conflit;
GOTO (@ 6 , d) = @1, a déjà été calculé, et il ne comporte pas de conflit;
@9 = GOTO (@ 6 , C) = {[C~cC., e)}, contient un seul item, donc ne comporte
pas de conflit.
Les dix ensembles de la collection LR (1), C = {@1, @2 1... @g} sont non
contradictoires. Donc, la grammaire est LR (1).
D'après le diagramme de la hiérarchie des grammaires LR de la Figure 125,
toute grammaire SLR (1) est nécessairement LR (1). On a déjà montré
précédemment que cette même grammaire est SLR (1) et que la collection LR (0)
correspondante comportait sept ensembles.
Il est fréquent pour différents ensembles d'items LR (1) pour une même
grammaire d'avoir les mêmes premières composantes (mêmes cœurs d'items), et
d'être différents sur leurs deuxièmes composantes. On remarque que pour la même
grammaire, chaque ensemble d'items LR (0) correspond à l'ensemble des
premières composantes (cœurs des items) d'un ou de plusieurs ensembles d'items
LR (1). C'est ce point qui est exploité par l'approche LALR pour remplacer
l'approche LR afin de diminuer considérablement le nombre de lignes de la table
d'analyse. L'idée est donc de construire les ensembles d'items LR (1) et de
fusionner les ensembles ayant un cœur commun, si aucun conflit ne se produit.
L'algorithme de construction de la table d'analyse LALR est semblable à celui
de la table d'analyse LR, il suffit donc de reconduire le même algorithme et de
fusionner ensuite les ensembles d'items ayant un cœur commun. Les trois paires
d'ensembles d'items concernées par cette fusion, sont (@3, @5), (@4, @1) et (@s,
@ 9 ). Dans les Tableaux XLII, XLIII et XLIV sont représentées respectivement
les tables d'analyse SLR (1), LR (1) et LALR (1) pour la grammaire donnée en
268 Chapitre 6

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

Tableau XLIII- Table d'analyse LR (1) pour la grammaire S' ~ S(o);


s ~ cd 1l; c ~ cd 2l 1d( 3)
Pour la même grammaire, le calcul de la collection canonique LR (0) montre
que la grammaire est SLR (1). De plus, on retrouve les mêmes ensembles d'items,
au nombre de sept, exactement comme avec LALR (1) quand on fusionne les
ensembles d'items présentant les mêmes cœurs. En effet, on a obtenu en premier
lieu la collection d'items LR (0) composée de sept ensembles d'items @a, @i, ... @5.
Cette collection d'ensembles d'items produit la table d'analyse SLR (1) formée de
sept lignes (sept états : 0, 1, 2, ... 6), du Tableau XLII.
Analyse syntaxique 269

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

Tableau XLIV - Table d'analyse LALR {1) pour la grammaire 8 1 ~ s(o);


s ~ cd 1) ; c ~ cd 2) 1d( 3)

Ainsi, en reconsidérant la collection LR (1) calculée précédemment et compte


tenu des trois paires d'ensembles d'items signalées ci-dessus, à savoir (@ 3 , @ 6 ),
(@4, @1) et (@ 8, @g), l'opération de fusion produit les lignes nommées 3-6, 4-7 et
8-9 de la table du Tableau XLIV. Ces dernières correspondent respectivement
aux lignes 3, 4 et 6 de la table d'analyse SLR {1) du Tableau XLII.

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.

L'algorithme suivant traite tous les types de commandes mentionnés pour


mener jusqu'au bout une analyse montante selon le modèle bien connu
décaler /réduire.
270 Chapitre 6

Algorithme d'analyse montante basée sur les grammaires de la famille


LR
Positionner le pointeur ps sur le 1er symbole de la chaine à analyser w $
/ * w est suivie du symbole$ permettant de marquer la fin de l'analyse * /
Répéter
début
si Action [s, a] = Dj
/* le symbole s est un état et a est le symbole rencontré en entrée */
alors début
empiler (a);
empiler (j);
avancer (ps) /* incrémenter ps f- ps + 1 */
fin
sinon si Action [s, al = Rj avec A~ ~. (J)
alors début
dépiler 2 x l~I symboles ;
/ * soit alors s' le nouvel état après l'action 'dépiler' précédente */
empiler (A) ;
empiler (GOTO [s', A))
émettre n° j de la règle A~ ~. W, en sortie
fin
sinon si Action [s, A) = «Accepter»
alors retourner/* Succès de l'analyse */
sinon erreur ()
fsi
fsi
fsi
fin

On peut à présent tester quelques exemples. On reviendra sur les tables


d'analyse déjà construites. On peut, en l'occurrence, utiliser par exemple la table
d'analyse du Tableau XLI pour la grammaire {S'~S (o); S ~SaSb {l) le (2l}, ainsi
que celle du Tableau XLIV pour la grammaire S' ~ s(o); S ~ cc( 1l; C ~
cd2) 1 d{3).
Soit alors à vérifier, en appliquant l'algorithme ci-dessus, si les chaines "aabb"
et "cdcd" appartiennent respectivement aux langages engendrés par les deux
grammaires sus-mentionnées.
Etat Entrée Action
0, aabb$, R2
OSl, aabb$, D2
0Sla2, abb$, R2
0Sla2S3, abb$, D4
0Sla2S3a4 bb$ R2
0Sla2S3a4S6 bb$ D7
Analyse syntaxique 271

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

Tout comme avec les exemples décrits auparavant, on vérifie la condition


LR (1) parallèlement au calcul de la collection canonique des ensembles d'items
LR (1).
@o = [8 1-7.8, E]
[8-7.aAd, E]
272 Chapitre 6

[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

Le deuxième exemple envisagé consiste à montrer qu'il est possible d'effectuer


une analyse montante déterministe avec une grammaire ambiguë. Comme
préconisé, il est question ici d'exploiter la précédence d'opérateurs.
Soit alors la grammaire ambiguë (augmentée) qui engendre le langage des
expressions arithmétiques simples non signées et parenthésées décrite par les
règles suivantes :
E' ~E(o)
E ~ E +E (l) 1 E * E (2) 1 (E) (3l 1 a (4)
On calcule la collection canonique LR (0) correspondante non pas dans le but
de vérifier si elle est LR, puisqu'elle est ambiguë et ne peut donc pas être LR,
mais pour construire la table d'analyse SLR (1) associée. Cette dernière, comme
on pourra le voir, présentera certaines cases multi définies dues au non
déterminisme de la grammaire (la grammaire est ambiguë, donc elle ne peut être
que non déterministe). La table d'analyse correspondante est donnée dans le
Tableau XL V. En appliquant la procédure de construction habituelle, la
collection LR (0) est représentée par la séquence d'ensembles d'items @ 0, @ 1, ... ,@ 9,
comme suit:
@o = {E' ~ .E
E ~ .E + E
E ~ .E * E
E ~ .(E)
E ~.a}
@1 = GOTO (©o, E)
@o = {E' ~ E.
E ~ E. + E
E ~ E. * E}
@2 = GOTO (@o, ()
@2 = {E ~ (.E)
E ~ .E + E
E ~ .(E)
E ~.a}
@3 = GOTO (@o, a)
@3 = {E ~a.}
@4 = GOTO (@1, +)
@4 = {E ~ E +.E
E ~ .E + E
E ~ .E * E
E ~ .(E)
E ~.a}
@5 = GOTO (@1, *)
@5 = {E ~ E *.E
E ~ .E +E
E ~ .E * E
274 Chapitre 6

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

Tableau XL V - Table d'analyse SLR{l) pour la grammaire représentée par


l'ensemble des règles E 1 ~ E (o); E ~ E + E (l) 1 E * E (2) 1 {E) (3) 1 a (4)

On constate que sur le symbole d'entrée 11 + 11 à la ligne 7 de la table {état 7), il


y a un conflit décalage / réduction, mais il est levé au profit d'une réduction, car
c'est l'associativité de l'opérateur + qui détermine de manière non ambiguë, la
façon avec laquelle ce conflit est résolu. En effet, le fait que l'opérateur + est
associatif à gauche, l'action à exécuter est la réduction, qui l'emporte sur le
Analyse syntaxique 275

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 »

Analyse de la chaine 11 a + a * a" avec la table d'analyse du Tableau XL VI.

Etat Entrée Action


0 a+a*a$ D5
Oa5 +a* a$ R6
OF3 +a* a$ R4
OT2 +a* a$ R2
OEl +a* a$ D6
OE1+6 a* a$ D5
OE1+6a5 * a$ R6
OE1+6F3 * a$ R4
OE1+6T2 *a$ D7
OE1+6T2*7 a$ D5
OE1+6T2*7a5 $ R6
276 Chapitre 6

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

Tableau XL VI- 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) (5) la {6)}

On abordera dans la section 4 de ce chapitre le traitement des erreurs


syntaxiques qui est fondamental dans tout type d'analyseur syntaxique. On
parlera, en l'occurrence, de la détection des erreurs syntaxiques ainsi que de leur
gestion.

4 Traitement des erreurs syntaxiques


Les programmes peuvent contenir des erreurs à différents niveaux:
Niveau lexical, comme par exemple l'écriture erronée d'un identificateur, un
mot-clé ou un opérateur, etc.
Niveau syntaxique, comme par exemple une expression mal formée.
Niveau sémantique, comme par exemple l'incompatibilité entre opérateurs et
opérandes dans une expression, la non-conformité entre le type des paramètres
d'appels et le type des paramètres formels lors de l'appel d'une procédure ou
fonction, etc.
Analyse syntaxique 277

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

apparaissant à plusieurs endroits dans un programme engendre plusieurs types de


messages indiquant des erreurs secondaires. Ce type de message peut disparaitre
dès lors que l'entité en cause est corrigée.

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.

Comme indiqué ci-dessus, les erreurs, dites secondaires, ne sont que la


propagation de certaines erreurs de syntaxe. Les erreurs de ce type appartiennent
à la catégorie des erreurs sémantiques. En effet, une erreur syntaxique (entité
malformée, par exemple), apparaissant à plusieurs endroits, par exemple en tant
que paramètre formel d'une procédure ou en tant que variable dans une formule,
se transforme en erreur sémantique, et plusieurs messages plus ou moins explicites
lui seront ainsi associés.
Si la détection des erreurs syntaxique reste un problème entièrement formel,
celui de leur gestion demeure informel et ouvert. Un compilateur adopte le mode
ou la stratégie qui répond au mieux aux circonstances de l'erreur. Il peut en
l'occurrence, adopter le mode « panique » en ignorant une partie du flot d'entrée,
ce qui ne met pas à l'abri de l'apparition d'erreurs secondaires en conséquence. Il
peut également appliquer d'autres stratégies comme le mode «production
d'erreurs » ou mode "syntagme", etc. Le choix d'une stratégie est relatif au
bénéfice et/ou à l'inconvénient qu'elle implique comparativement aux autres
stratégies. Par exemple, si on applique le mode « panique » dans la partie
déclaration d'un programme, il est évident que le saut jusqu'au prochain symbole
de synchronisation (une virgule ou un point-virgule), omet une bonne partie des
entités déclarées et implique inévitablement l'apparition d'erreurs secondaires. Il
serait donc plus raisonnable d'éviter le mode « panique » dans cette partie
sensible d'un programme et d'opter plutôt pour le mode «production d'erreurs»
qui éviterait au mieux l'apparition d'erreurs secondaires.
On donnera ci-après deux exemples pour montrer quelles sont les actions
appropriées à appliquer pour gérer de manière efficace et fiable certaines erreurs
de syntaxe. Le premier exemple concerne l'analyse par la table prédictive LL (1) ;
le second exemple s'appuie sur la méthode de précédence d'opérateurs.

4.1 Cas de l'analyse prédictive LL (1)


On localise une erreur syntaxique lorsque le symbole d'entrée (celui attendu), ne
coïncide pas avec celui du sommet de pile (M [a, a] = 0) ou lorsque le symbole
d'entrée et le non-terminal A, au sommet de pile donne M [A, a] = 0.
Analyse syntaxique 279

On peut montrer en quelques points essentiels comment l'analyseur se


récupère rapidement en pratique des erreurs susceptibles de se produire :
On peut mettre Follow1 {A) dans l'ensemble de synchronisation.
L'utilisation de Follow1 n'est pas toujours suffisante, car l'oubli d'un point-
virgule 11 ; 11 après une instruction d'affectation peut provoquer le saut du mot-
clé ou de l'identificateur commençant l'instruction suivante. Il faut alors
ajouter à l'ensemble de synchronisation les symboles qui commencent les
instructions suivantes.
Ajouter First 1 (A) à l'ensemble de synchronisation, car il est possible de
reprendre par le symbole non-terminal A si le symbole d'entrée coïncide avec
First1 (A).
On peut utiliser par défaut A ~ E {si elle existe), pour différer l'erreur afin
d'accélérer l'analyse.
Si un terminal t appartient au sommet de pile, il va falloir dépiler le terminal
en question (dépiler ( t)), et poursuivre l'analyse.
Un symbole (terminal) d'entrée, indiquant une case vide de la table d'analyse,
sera sauté (ignoré, donc supprimé) et poursuivre l'analyse.
On reconsidère la table d'analyse prédictive LL {1) donnée en exemple
auparavant (Tableau XXXIV), correspondant à la grammaire représentée par
l'ensemble des règles de l'ensemble P ci-dessous. On la complète par l'ensemble
des symboles de synchronisation en prévision des éventuelles erreurs qui peuvent
se produire. La table d'analyse prédictive complète modifiée avec la prise en
compte des symboles de synchronisation est donnée par le Tableau XL VII.
L'action de synchronisation est indiquée par sync dans la table d'analyse associée.

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

Tableau XL VII - 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 [Aho, 86]

Soit à tester l'expression erronée 11 ) a* +a 11


280 Chapitre 6

Pile Entrée Remarque


E$ )a*+a$ on ignore 11 ) 11 et on saute au prochain symbole
E$ a*+ a$ reprise avec First 1 (E).
TM$ a*+ a$
FNM$ a*+ a$
aNM$ a*+ a$ coïncidence
NM$ *+a$
*FNM$ *+a$ coïncidence
FNM$ +a$ erreur M [F, + J = sync
NM$ +a$ F, a été dépilé (supprimé)
M$ +a$
+TM$ +a$ coïncidence
TM$ a$
FNM$ a$
aNM$ a$ coïncidence
NM$ $
M$ $
$ $
Au final, l'analyseur basé sur une telle stratégie peut mener le traitement de cette
expression jusqu'au bout sans interruption, si ce n'est que de temps à autre, il
renvoie un message pour signaler la présence d'une erreur. Les messages
avertissent un programmeur afin de l'aider à corriger son programme.

4.2 Cas de l'analyse basée sur la précédence d'opérateurs


Il existe précisément deux configurations distinctes où un analyseur basé sur la
précédence d'opérateurs est susceptible de détecter une erreur syntaxique :
Lorsqu'il n'existe pas de relation de précédence entre l'élément du sommet de
pile et le symbole d'entrée.

Lorsqu'il n'y a aucune règle de production qui permet d'effectuer une


réduction pour une «prise» (sous-chaine) apparaissant dans la pile de
l'analyseur.
Par exemple, quand on a dans la pile la configuration $E + T, il y a deux
prises qui sont candidates. En effet, on peut réduire, soit la prise E + T au non-
terminal E, en utilisant la règle E ~ E + T, soit la prise T avec la règle E ~ T.
La question de savoir quelle est celle des deux qui sera choisie ne se pose pas,
puisqu'ici, on a à faire à un analyseur déterministe qui indiquera sans ambigüité,
laquelle des deux prises sera sélectionnée pour la réduction.
Dans l'expression 11 ) a* + a", la parenthèse ouvrante 11 ) 11 n'a pas de relation de
précédence avec $ (symbole du fond de la pile), donc l'erreur est localisée en
temps réel. En revanche, dans l'expression 11 a + * a", l'erreur n'est pas détectée à
priori en temps réel, car il y a une relation de précédence entre les opérateurs + et
* (+ <:: *, d'après la table de précédence d'opérateurs) qui indique d'empiler
l'opérateur * et de poursuivre l'analyse. L'expression 11 a + * a" est
syntaxiquement incorrecte, mais si on décide d'empiler l'opérateur *, comme
indiqué par la relation de précédence + <:: *, on ne fera que repousser le traitement
Analyse syntaxique 281

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

Tableau XL VIII - Table de précédence d'opérateurs des expressions avec


les cas d'erreurs
E1 : /* Cette routine est sollicitée quand l'expression toute entière est
manquante. */ Elle insère l'opérande "a", ensuite émet en sortie le message
"Opérande attendu".
E2: /* Cette routine est sollicitée lorsqu'une expression commence par une
parenthèse fermante. */ Elle supprime la parenthèse fermante ")" en entrée, et
émet le message "Parenthèse fermante non équilibrée".
E3 : /*
Cette routine est sollicitée quand un opérande "a" ou une parenthèse
fermante ")" est suivi(e) d'un opérande ou d'une parenthèse ouvrante "(". */
Elle procède à l'insertion de l'opérateur "+", et émet en sortie le message "
Opérateur manquant".
E4 : /* Cette routine est sollicitée quand une expression se termine par une
parenthèse ouvrante " (". */ Elle supprime la parenthèse ouvrante " (" de la
pile, ensuite émet en sortie le message "Parenthèse fermante manquante".
On peut donc, à présent tester une expression erronée pour voir comment
fonctionne ce modèle de traitement des erreurs. On choisit délibérément une
expression qui montre que les cas d'erreurs répertoriés dans la table de précédence
sont insuffisants pour régler tous les cas d'erreurs. Ainsi, l'analyse de l'expression
erronée "a *)" par la technique de précédence d'opérateurs montre qu'au bout
d'un certain nombre de pas d'analyse, l'analyseur est dans une configuration
indiquant un cas erreur non prévu dans le répertoire (E1, E2, E3 et E4) de la
table d'analyse de précédence d'opérateurs avec les cas d'erreurs du
Tableau XL VIII. Cette configuration n'est autre que la suivante :
282 Chapitre 6

Pile Chaine d'entrée Action


$S* )$ Erreur
Mais, comme la relation * > ), indique une réduction impossible à effectuer (il
n'existe aucune prise qui lui correspond dans la pile), par conséquent, le
gestionnaire d'erreurs émet le message "Opérande omis", et effectue la réduction
comme si c'était la prise "S * S" qui se réduit à S. On a, en fin de compte, la
nouvelle configuration de l'analyseur qui indique à nouveau une erreur, car il
n'existe pas de relation de précédence entre "$" et 11 ) 11 • La nouvelle configuration
est donc la suivante :
Pile Chaine d'entrée Action
$S )$ Erreur
On constate que cette erreur (notée E2) est déjà répertoriée dans la table de
précédence. La routine E2 supprime la parenthèse ")" et émet le diagnostic
"parenthèse fermante non équilibrée". On obtient donc la configuration finale
d'acceptation suivante :
Pile Chaine d'entrée Action
$S $ "Acceptation"
Cette dernière indique que l'analyseur, en coopération avec le gestionnaire
d'erreurs, a fini par analyser la chaine présentée en entrée. Cette manière de
procéder n'est pas très rigoureuse comme on peut le constater, du fait que
l'analyseur agit de manière passive en laissant momentanément une erreur se
produire et ne la corrige que de manière différée. Pour y remédier, on introduit
une technique ajoutant une information évitant de laisser une erreur se produire
sans être corrigée instantanément. Une stratégie très rigoureuse consiste à
construire un automate d'états d'entrée dans les expressions. Cette technique est
décrite dans le paragraphe ci-après.

Technique de détection des erreurs par un automate d'entrée dans les


expressions
Soit la grammaire qui engendre les expressions arithmétiques parenthésées signées
ou non décrite par l'ensemble des règles suivant :
E ~A 1+A1-A
A~TIA+TIA-T
T ~ F 1T *F 1 T /F
F~PIPÎF
P ~a 1 (E)

L'automate d'entrée de cette grammaire est représenté par la table du


Tableau XLIX.
Les états sont 0, 1 et 2. L'état 0 représente l'état initial de l'automate. Les
cases vides représentent des erreurs.
Par exemple, à l'état 0, une expression ne peut jamais commencer par un
opérateur comme *, / ou Î. Ceci est naturellement bien connu, puisqu'on ne
connait pas d'expression arithmétique qui commence par un opérateur
Analyse syntaxique 283

multiplicatif ou d'exponentiation. L'expression ne commence pas, non plus, par la


parenthèse fermante ")" ou le symbole spécial $.

Tableau XLIX- Automate d'états d'entrée dans les expressions


arithmétiques

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.

Pile Chaine d'entrée Relation Action EC ES


$ a*+ a$ $<a shift 0 2
$a *+a$ a!>* reduce 2 1
$ *+a$ $<* shift 2 1
$* +a$ Erreur 1
$* a$ *<a shift 1 2
$*a $ a!>$ reduce 2 2
$* $ * !>$ reduce 2 2
$ $ «Succès» 2 2
EC et ES signifient respectivement état courant et état suivant au niveau de
l'automate d'entrée des expressions du Tableau XLIX.
284 Chapitre 6

Ainsi, se termine l'analyse avec succès en ayant localisé et traité, à l'occasion,


l'erreur en temps réel, et de surcroit en un nombre de pas raisonnable.
Mais, avant de clore définitivement ce chapitre, il convient de revenir sur la
table des symboles.

5 Table des symboles vue par l'analyse syntaxique


La table des symboles qui, pour rappel, constitue la mémoire du compilateur, a
été initialisée au niveau de l'analyse lexicale, avec les différentes entités
rencontrées dans le flot d'entrée (programme source). Ces entités sont les noms
des variables du programmeur comme les identificateurs, les noms des fonctions,
des procédures, les constantes, les mots-clés, etc.
Cette table doit être révisée et actualisée au cours de l'analyse syntaxique.
Donc, on doit pouvoir y accéder à tout moment pour y rechercher ou y ajouter
une entité ou une information relative à cette entité.
Les entrées dans la table des symboles ont généralement la forme de la
structure composée du couple <nom, attributs>, où les attributs peuvent être : le
type, la valeur, l'adresse, l'étendue, l'adresse d'une routine en cas d'erreur, etc.
La table des symboles doit être réalisée avec beaucoup de soin, car le
compilateur passe la moitié de son temps à la consulter. De ce fait, il serait
indispensable que l'organisation de cette structure soit maitrisée et que l'accès aux
informations qu'elle contient soit le plus souple possible. La collecte
d'informations ou le remplissage de la table des symboles a lieu lors des phases
d'analyse (partie frontale du compilateur). Par exemple, un identificateur est
sauvegardé dans la table des symboles pendant l'analyse lexicale. L'analyse
syntaxique peut ajouter à l'entrée dans la table de cet identificateur, certaines
informations clés (attributs) le concernant, comme le type, son utilisation (en tant
que variable, procédure, fonction ou étiquette), ainsi que son adresse en mémoire.
Evidemment, l'actualisation de la table ne s'arrête pas là, puisque d'autres
informations pourront y être rajoutées au fur et à mesure au cours des autres
phases d'analyse, particulièrement l'analyse sémantique et de la traduction.
Il est inutile de rappeler les détails concernant l'organisation et l'accès à la
table des symboles - ces deniers ont été intensivement étudiés et décrites dans le
chapitre 5 - mais, on doit néanmoins connaitre quel est l'apport de l'analyse
syntaxique pour cette fameuse table et quel est ainsi le rôle de cette dernière dans
les phases ultérieures du compilateur.
L'analyse lexicale, agit localement sur les entités qu'elle collecte sans se soucier de
leur juxtaposition ou de leur environnement. En effet, l'agencement des mots,
pour former des expressions et/ou des instructions, est laissé à la charge de
l'analyse syntaxique qui s'appuie sur une grammaire pour établir les liens entre
ces mots.
Si l'on adopte la démarche d'un compilateur multi-passes, où l'on suppose que
l'analyse lexicale constitue une phase séparée de l'analyse syntaxique, les entités
sont collectées indépendamment les unes des autres au cours de l'analyse lexicale.
Cela implique que l'on remplit la table des symboles sans connaitre au préalable
Analyse syntaxique 285

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

soit en plusieurs passes (analyseur multi passe) ;


soit en une seule passe.

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

retour 1- r, 3, E1T2F1a+E1T1Fi, a*T+E$ a$


alternative 1- r, 3, E1T2F1a+E1T1 1 F*T+E$ a$
1- s, 3, E1T2F1a+E1T2 1 F+E$ a$
1- s, 3, E1T2F1a+E1T2F1, a+E$ a$
a=a 1- s, 4, E1T2F1a+E1T2F1a, +E$ $
*
$ + 1- r, 4, E1T2F1a+E1T2F1a, +E$ $
retour 1- r, 3, E1T2F1a+E1T2F1, a+E$ a$
1- r, 3, E1T2F1a+E1T2, F+E$ a$
alternative 1- r, 3, E1T2F1a+Ei, T+E$ a$
1- s, 3, E1T2F1a+E2, T$ a$
1- s, 3, E1T2F1a+E2T1, F*T$ a$
1- s, 3, E1T2F1a+E2T1F1, a*T$ a$
a=a 1- s, 4, E1T2F1a+E2T1F1a, *T$ $
$:t:* 1- r, 4, E1T2F1a+E2T1F1a, *T$ $
retour 1- r, 3, E1T2F1a+E2T1F1 1 a*T$ a$
alternative 1- r, 3, E1T2F1a+E2T1, F*T$ a$
1- s, 3, E1T2F1a+E2T2, F$ a$
1- s, 3, E1T2F1a+E2T2F1, a$ a$
a=a 1- s, 4, E1T2F1a+E2T2F1a, $ $
1- t, 4, E1T2F1a+E2T2F1a, e e
On obtient la chaine 11 E1T2F1a+E2T2F1a 11 dans Pilel qui produit la dérivation
canonique 1t1 = 1 4 5 2 4 5. L'analyse est réalisée en 36 pas.

Méthode de la table prédictive LL (1)


Pour rappel, la grammaire est définie par les règles : E ~ TM (t) ; T ~ FN <2l ;
M ~ + TM (3) 1 e (4 ) ; N ~ * FN (5) 1 E (B) ; F ~ a (7) 1 (E) (B)
La table d'analyse prédictive correspondante est donnée par Tableau XXXIV du
présent chapitre.
E$, a+ a$, E configuration initiale
TM$, a+ a$, 1 dérivation règle 1
FNM$, a+ a$, 2 dérivation règle 2
aNM$, a+ a$, 7 dérivation règle 7
NM$, +a$, E coïncidence ; lire caractère suivant
M$, +a$, 6 dérivation règle 6
+TM$, +a$, 3 dérivation règle 3
TM$ a$ E coïncidence ; lire caractère suivant
FNM$, a$, 2 dérivation règle 2
aNM$ a 7 dérivation règle 7
NM$, $, E coïncidence ; lire caractère suivant
M$, $, 6 dérivation règle 6
$, $, 4 dérivation règle 4
$, $ E « Succès»
La dérivation canonique 1t1 = 1 2 7 6 3 2 7 6 4 est la trace de l'analyse de
11 a + a". L'analyse est réalisée en 13 pas. L'analyse de la même expression avec la

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.

Utilisation d'un automate à pile déterministe


Si on reconsidère l'automate à pile de la Figure 123, l'analyse de "a + a"
produit la séquence suivante :
0, a+ a$, # I-
l, +a$, # I-
o, a$, # l-
1, $, # 1-
t. E E Stop
L'automate a atteint son état final d'acceptation au bout de 4 pas d'analyse.

Méthode de précédence d'opérateurs


Sur la base de la table des relations de précédence de la Figure 121, l'analyse
ascendante déterministe réalisée sur l'expression "a + a", en utilisant la
grammaire ambiguë S ~ S + S {l); S ~ S * S <2); S ~ (S) (3); S ~ a <4l, correspond
à la séquence suivante :
$ a $<::a shift
$a + al>+ reduce règle 4
$S + $ <:: + shift
$S+ a + <:: a shift
$S+a $ a<:: $ reduce règle 4
$S+S $ + ::> $ reduce règle 1
$S <:: Succès analyse= 4 4 1, donc 1tr = 1 4 4
L'analyse s'achève après 6 pas.

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

La traduction constitue la phase ultime du processus de compilation. C'est


une phase partagée entre la partie frontale et la partie finale du compilateur.
Plus précisément, la traduction commence à la fin de la partie frontale, on
parle alors de génération de code intermédiaire. On parle aussi de traduction
durant la partie finale, pour désigner la génération de code cible. 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. 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.

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

Le rôle de la représentation intermédiaire est capital dans la portabilité des


compilateurs. En effet, certains compilateurs effectuent des traitements
substantiels (pour optimiser la représentation intermédiaire) que l'on peut
qualifier de partie centrale, indépendante à la fois du langage source et de la
machine cible. On peut ainsi produire des compilateurs pour toute une gamme de
langages et de machines en combinant la partie centrale avec une partie frontale
par langage source et une partie finale par langage cible.

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.

Définition 2.1 (Forme post-fixée)


Cette représentation (forme post-fixée) est également appelée forme polonaise
inverse en référence au logicien Polonais Jan tukasiewicz qui a inventé la forme
préfixée nommée également notation polonaise.
On s'intéresse ici plus particulièrement à la notation post-fixée des
expressions. On introduira ensuite son extension à certaines instructions,
notamment les instructions de contrôle, comme les instructions conditionnelles, les
boucles, etc.
La notation post-fixée d'une expression est définie formellement comme suit :
Si E correspond à une variable ou une constante, la notation post-fixée de E
est représentée par le terme E lui-même.

Si E = Ei o E2 où o est un opérateur binaire, alors la notation post-fixée de


l'expression E est définie par E' = E'1 E'2 o, avec E'1 et E'2 qui sont
respectivement les notations post-fixées des expressions E 1 et E 2.

Si E = o Ei où o est un opérateur unaire (ou monadique), alors E' = E' 1 o,


avec E'1 qui est la notation post-fixée de E 1.

Si E = (E1), alors l'expression post-fixée de E est notée E' = E' 1. L'expression


E' 1 est la notation post-fixée de l'expression E 1.

Pour lever toute équivoque sur la dénomination (forme polonaise inverse)


couramment utilisée pour désigner la forme post-fixée, on définit la forme
Traduction 291

préfixée qui n'est pas, à proprement parlé, l'inverse de la forme post-fixée. On


verra que cette inversion concerne la position des opérateurs par rapport aux
opérandes au niveau de l'expression. La position des opérandes est la même.

Définition 2.2 (Forme préfixée)


Cette notation, comme mentionné ci-dessus, est surnommée également notation
polonaise. Elle est définie formellement comme suit :

Si E correspond à une variable ou une constante, la notation préfixée de E est


représentée par le terme E lui-même.

Si E = E1 o E 2 où o est un opérateur binaire, alors la notation préfixée de


l'expression E est définie par E' = o E 11 E'2, avec E 11 et E 12 qui sont
respectivement les notations préfixées des expressions E1 et E 2.

Si E = o E1 où o est un opérateur unaire (ou monadique), alors E' =o E'i,


avec E 11 qui est la notation préfixée de E 1.

Si E = (E1), alors l'expression préfixée de E est notée E 1 = E 1.1 L'expression


E 11 est la notation préfixée de l'expression E 1.
Dans la notation post-fixée, les opérateurs apparaissent après leurs opérandes.
En effet, par exemple, 11 5 + 7 11 , se transformera en 11 5 7 + 11 en notation post-fixée.
Avec la forme infixée on aura plutôt l'inverse, c'est-à-dire 11 + 5 7 11 •
Un avantage des représentations polonaises est l'absence de parenthèses qui sont
requises par la notation infixée des expressions. En effet, avec la notation infixée
11 5 - 7 * 9 11 on peut également écrire 11 5 - (7 * 9) 11 , qui est complètement
différente de 11 (5 - 7) * 9 11 • Avec la notation post-fixée, la première expression, à
savoir, 11 5 - 7 * 9 11 s'écrit 11 5 7 9 * - 11 1 qui signifie sans ambiguïté 11 5 (7 9 *) - 11 qui
se réduit à 11 5 63 - 11 , et produit 5 - 63 = 58. En revanche, la deuxième expression,
c'est-à-dire 11 (5 - 7) * 9 11 , s'écrit 11 5 7 - 9 *11 , qui signifie sans ambiguïté 11 (5 7 -) 9
*11 , qui devient 11 - 2 9 *11 c'est-à-dire (-2) * 9 = -18.
Malgré son nom, la notation polonaise inverse (post-jixée) n'est pas l'inverse
(au sens image miroir) de la notation polonaise (préfixée). On le vérifie
facilement avec les opérateurs non commutatifs. En fait, l'expression infixée
11 4 / 2 11 (di vision de 4 par 2) a pour forme polonaise (préfixée) 11 / 4 2 11 et pour
forme post-fixée 4 2 /". Cette dernière a pour valeur 2, tandis que 2 4 / 11 qui est
11 11

l'inverse (image miroir) de 11 / 4 2 11 ) a pour valeur 1/2.


On peut également obtenir la notation post-fixée pour n'importe quelle
expression, il suffit de parcourir l'arbre abstrait correspondant en marquant au
passage chaque nœud visité se trouvant à gauche du contour de l'arbre en
question.
L'arbre de la Figure 126 correspond à l'arbre abstrait de l'expression 11 6 + 1 / 7 11 •
Son parcours en post-ordre, c'est-à-dire en profondeur de gauche à droite, en
marquant au passage chaque nœud visité, produit la forme post-fixée notée 11 6 1 7
/ + 11 de l'expression infixée 11 6 + 1 / 7 11 • Autrement dit, étant donné un arbre
292 Chapitre 7

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.

Figure 126 : Parcours en post-ordre de l'arbre abstrait de l'expression


11
6 +1/ 7 11

Tl T2

Figure 127: Arbre binaire abstrait de l'expression Tl p T2

Il faut souligner l'importance de la récursivité du parcours de l'arbre. En


d'autres termes, lorsqu'on traite le sous-arbre Tl (respectivement T2), on
reconduit exactement le même type de parcours que celui de l'arbre de racine p.
Le processus est répété jusqu'à visiter tous les nœuds de l'arbre selon le type de
parcours choisi (pré-ordre ou post-ordre).
Par exemple, le parcours en pré-ordre de l'arbre de la Figure 128 fournit
l'expression préfixée 11 - Tl T2 11 qui produit 11 - * 2 4 / 5 2 11 lorsqu'on développe
successivement le parcours sur Tl (qui donne * 2 4), ensuite sur T2 (qui donne
/ 5 2).

2 /*' 4
//'
5 2

Figure 128 : Arbre binaire abstrait de l'expression 11 2 *4 - 5 / 2 11

De même, lorsqu'on effectue un parcours en post-ordre du même arbre, on


obtient l'expression post-fixée 11 Tl T2 - 11 qui produit ensuite 11 2 4 * 5 2 / - 11 •
On présente ci-dessous les versions génériques des algorithmes de parcours pré-
ordre et post-ordre.
La forme intermédiaire étant une représentation abstraite et réduite d'un
programme ; il faudrait alors penser à une structure de données adéquate pour son
implémentation (représentation en machine). On connait deux représentations
Traduction 293

internes intéressantes pour implémenter ce type d'arbre, à savoir, la


représentation statique et la représentation dynamique. La première, est une
implémentation par table. La seconde, est une implémentation par liste chainée.
Le Tableau L et la Figure 129, illustrent respectivement une représentation
interne statique et une représentation interne dynamique de l'arbre abstrait de la
Figure 128.

Procédure pré_fix ( b : arbre_ bin) ;


début
si b i- NIL alors début
écrire ( b~. info) ;
pre_fix ( b~ .gauche) ;
pre_fix ( b~. droit)
fin
fin;

Procédure post_jix (b : arbre_ bin) ;


début
si b i- NIL alors début
post_fix (b ~.gauche) ;
post_ fix ( b ~.droit) ;
écrire ( b ~. info) ;
fin
fin;

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

Tableau L - Représentation statique tabulaire de l'arbre de la Figure 128

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 / /

Figure 129 : Représentation dynamique de l'arbre de la Figure 128

S'il est possible de traduire l'expression a * (b + c) en écrivant P (a* ( b + c))


=a b c + *, conformément à la définition de base de la notation post-fixée, il
devrait être de même pour la plupart des autres constructions. Pour cela, il va
falloir garder à l'esprit, le modèle dédié aux expressions et comprendre, par
ailleurs, le sens de la construction pour laquelle on voudrait obtenir la forme post-
fixée. Ainsi, par exemple, l'instruction conditionnelle : if <condition> then <Il>
else <12> possède pour modèle post-fixé, la notation suivante :
P (if <condition> then<ll>else<12>) =P (<condition>) @1 BZ P (<Il>) @2
BR P (<12>)
où:
P (<condition>) est la forme post-fixée de la condition. La condition est
généralement formulée par une expression logique. Par exemple, (a > b * c) or
(y and true) est une expression logique. On peut imaginer diverses formes
d'expressions logiques plus complexes, mais cela dépend des possibilités du
langage.
"BZ" est un code qui représente un branchement conditionnel "Branch on
zero" vers l'adresse dénotée par le terme @1. On voit bien ici le respect de
l'esprit de la notation post-fixée, par le fait que le branchement "BZ"
représente un opérateur situé après l'adresse @1. Cette dernière est considérée
comme un opérande, d'où l'écriture "@1 BZ".
L'adresse @1 représente l'endroit où doit commencer l'instruction P ( <12> ).
P (<Il>) et P ( <12>) sont les représentations post-fixées des instructions
<Il> et <12>, respectivement.
"BR" est un code qui représente un branchement inconditionnel vers l'adresse
dénotée par le terme @2.
L'adresse @2 représente l'endroit où doit commencer l'instruction qui suit
immédiatement l'instruction P ( <12> ).
Traduction 295

On peut récapituler en indiquant par des flèches, la direction du branchement


et l'adresse vers laquelle sera effectué éventuellement le branchement, comme
suit:
P (<condition>) @1 BZ P (<Il>) @2 BR P ( <12>) _)
L (: ...-A

On voit donc, d'après cette construction que la compréhension de la


sémantique d'une instruction aide sensiblement le concepteur à structurer et à
réaliser la traduction adéquate selon la forme intermédiaire envisagée.
L'instruction conditionnelle précédente <if_ then_ else> constitue
l'instruction de contrôle de base pour les autres instructions comme < while _do>,
<if_ then>, <repeat_ until>, etc. Il est donc possible d'étendre le modèle de
forme intermédiaire associé à l'instruction <if_ then_ else>, à toutes les autres
instructions de contrôle. Il suffit de reconduire le formalisme de base
(<if_ then_ else>) en y ajoutant les différences avec les autres instructions de
contrôle. Pour illustrer, on décrit ci-après les formes post-fixées de quelques
instructions de contrôle parmi les plus courantes comme if <condition> then
<I>, while <condition> do <I>, etc.
L'alternative simple selective if <condition> then <I>. Ici, le choix est unaire
contrairement à « if <condition> then <Il> else <12> » où le choix est
binaire. Donc, il est facile de s'en inspirer et de faire en sorte que la deuxième
alternative, à savoir, la clause <12> correspondant au else soit vide. L'adresse
de branchement prévue dans l'instruction if <condition> then <Il> else
<12>, lorsque la condition n'est pas vérifiée (Branch on zero), sera confondue
avec l'adresse qui suit immédiatement P (<Il>), puisque <12> n'existe pas
(est vide). Donc, il n'y a même pas lieu, avec l'instruction « if <condition>
then <I> », d'avoir le branchement inconditionnel BR prévu à l'adresse @2,
car il devient superflu. En conséquence, on aura le formalisme simplifié
P (<condition>) @ BZ P ( <I> ). Ici, @ représente l'unique adresse qui
représente à la fois, l'adresse de branchement lorsque la condition <condition>
n'est pas vraie, et l'adresse qui vient immédiatement après P ( <I> ). D'où,
schématiquement, avec des flèches, on a : P (<condition>) ~ BZ P ( <I> J.
La Boucle (répétition) « while <condition> do <I> ». De même ici, le choix
est unaire, mais il va se répéter tant que la condition <condition> reste vraie.
La répétition de l'exécution de l'instruction <I> est assurée par une
instruction de branchement inconditionnel qui suit immédiatement
l'instruction <I>. Le branchement aura lieu vers l'adresse où commence
l'expression de la condition P (<condition>). On aura ainsi la forme
appropriée P (<condition> )@1 BZ P ( <I>) @O BR. Pour voir comment est
structurée la forme post-fixée de la boucle « while <condition> do <I> », on
utilise à nouveau les flèches pour mettre en valeur graphiquement la direction
des branchements et l'adresse vers laquelle sera effectué le branchement
comme suit:
P edition> )@1 BZ P ( <I>) @O BR .)
c.: ?:::>
296 Chapitre 7

Là aussi, on voit une forme post-fixée où :


@1 est l'adresse de branchement conditionnel. L'adresse @1 est celle où la
condition n'est pas vraie (Branch on zero) ; donc c'est une adresse qui vient
après BR, d'où l'on ne doit pas revenir tester une nouvelle fois la condition
<condition>, car c'est une sortie définitive de la boucle.
@O est l'adresse de la condition P (<condition>) ; on y vient
inconditionnellement par l'instruction BR, afin de tester une nouvelle fois la
condition <condition>.
En définitif, on pourra reconduire le même formalisme pour chaque structure
de contrôle dont on connait la sémantique. Il suffit de reconsidérer la démarche
décrite ci-dessus en respectant scrupuleusement la signification de la structure de
contrôle ainsi que le type de représentation intermédiaire adopté.
On donne ci-dessous une version générique (squelette) d'un algorithme de
simulation du comportement d'une machine à pile. On se limite à quelques
opérandes : binaires, unaires, de branchement, etc.
Soit alors post un vecteur représentant la forme post-fixée et soit i la variable
représentant la position dans le vecteur post.
i~ 1;
tantque non-fin de post(i] faire
début
cas de post(i]
opérande : empiler (post( i]) ;
*
opérateur binaire BZ
début
Dépiler (opérande!) ; Dépiler (opérande2) ;
Générer (post(i], R, opérande!, opérande2) ;
Empiler (R)
fin
opérateur unaire BR*
début
Dépiler (opérande) ;
Générer (post(i], R, opérande) ;
Empiler (R)
fin
BZ : si pile (sommet) = false
aiorf début
Dépiler (x) ; Aller à post[ i - 1]
fin
fsi ;
BR : Aller à post( i - 1)
fin
i ~ i +1
fin
fait ;
Traduction 297

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.

Notion d'arbre abstrait


Un arbre abstrait est considéré comme la forme la plus générale des trois formes
intermédiaires évoquées (post-fixée, arbre abstrait et code à trois adresses). En
effet, contrairement à la forme post-fixée qui est une représentation linéaire dédiée
à une machine à pile, l'arbre abstrait peut être interprété ou traduit sur n'importe
quel type de machine. La forme post-fixée peut être obtenue par un simple
parcours en profondeur post-ordre, tout comme il est possible d'obtenir à partir
du même arbre abstrait également la forme préfixée moyennant un parcours en
profondeur pré-ordre. Comme son nom l'indique, l'arbre abstrait est une structure
arborescente qui donne une image des différentes constructions telles qu'elles sont
agencées hiérarchiquement dans un programme source. C'est une forme réduite de
l'arbre de dérivation ou de l'arbre syntaxique. Son exploitation, en vue d'une
interprétation ou d'une traduction en code cible, est généralement réalisée par un
parcours en profondeur.
Ci-après, sont fournis les arbres abstraits, respectivement, de l'expression
booléenne (a * 2 > c) and ( b < 5) et de l'instruction conditionnelle if (a * b > 2)
and ( c = d) then x := x + 1 else x := x - 1.
298 Chapitre 7

En ce qui concerne l'expression booléenne, on a à faire à un arbre abstrait


représenté par un arbre binaire comme celui de la Figure 130. D'ailleurs, tous les
opérateurs qui représentent des nœuds internes de l'arbre sont des opérateurs
binaires ou unaires (ici ils sont tous binaires). On a trois types d'opérateurs :
arithmétiques (*), relationnels ( < et >) et logiques (and).

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.

< if_ then_ else >

Figure 131 : Arbre abstrait de la condition "if (a * b > 2) and ( c = d)"


Traduction 299

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.

Définition 2.3 (Code à trois adresses)


Le code à trois adresses est subdivisé généralement comme une structure à
quatre champs définis comme suit :
Deux champs sont destinés pour contenir les opérandes, en l'occurrence,
l'opérande1 et l'opérande2.
Un champ pour l'opérateur. Celui-ci peut-être un opérateur arithmétique,
logique, relationnel, de branchement, etc. Par ailleurs, il peut être binaire
ou unaire.
Un champ pour stocker le résultat de l'opération en question. En général
ce champ est désigné par une variable temporaire.
Concernant la disposition des champs du code lui-même, deux formats ont été
proposés et utilisés. Certains auteurs adoptent la disposition des quatre champs
selon le format: «Opérateur, Opérandel, Opérande2, Résultat». Par exemple,
l'expression "(a* 2 > c) and (b < 5) " a pour code à trois adresses les séquences
de code suivantes :
(1) *1 a, 2, Tl
(2) >, Tl, c, T2
(3) <, b, 5, T3
(4) and, T2, T3, T4
Ce code est a été numéroté pour pouvoir référencer, au besoin, chaque
séquence de code.
D'autres auteurs, préfèrent utiliser plutôt une disposition des champs, plus
naturelle en explicitant l'opération selon le format « Résultat :=, Opérandel,
Opérateur, Opérande2 »,où le méta symbole ": =" n'est pas considéré comme un
champ du code, mais il indique symboliquement que le résultat de l'opération est
affecté à la variable temporaire nommée "Résultat". Dans cet ouvrage
particulièrement, on optera plus souvent pour le format : « Opérateur,
Opérandel, Opérande2, Résultat ».
Le même exemple d'expression fournit un code à trois adresses selon le
deuxième format comme suit :
Tl:= a* 2
T2 :=Tl> c
T3 := b < 5
300 Chapitre 7

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.

C'est un code facile à obtenir, car il correspond à un découpage naturel des


opérations dans le sens de leur exécution.

Il est relativement lisible et facile à comprendre que ses deux homologues,


arbre abstrait et représentation post-fixée. De ce fait, il sera relativement plus
aisé d'associer les routines adéquates nécessaires à la génération de code cible
final.

La structure de données en table comportant généralement quatre champs


(opérateur, opérandel, opérande2, résultat) offre un accès facile. Elle sera
surtout facile à exploiter lors des phases de traduction en code cible. Les
quadruplets sont repérés chacun par son adresse (indice) dans la table.

Il est beaucoup plus proche du code d'une machine à registres


comparativement aux autres formes intermédiaires ; ce qui permet une
traduction finale en code cible plus simple.

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.

Il faut noter également que chaque modèle de traduction (forme post-fixée,


arbre abstrait ou code à trois adresses), peut être appliqué aussi bien avec un
analyseur syntaxique ascendant qu'avec un analyseur syntaxique descendant. En
conséquence, il sera toujours possible de réaliser un analyseur syntaxique mixte
(ascendant-descendant ou descendant-ascendant) en fixant la forme intermédiaire
que l'on voudrait générer. Ce type de combinaison peut arriver quand on veut
améliorer, par exemple, la vitesse du processus d'analyse syntaxique. En effet, on
peut bien appliquer un analyseur syntaxique descendant basé sur une descente
récursive pour sa simplicité, mais à cause de la lenteur de la récursivité on opte
pour une analyse ascendante au niveau des parties où sont traitées, par exemple,
les expressions (arithmétiques, logique, etc.). L'analyse ascendante s'appuie ici sur
la précédence (priorité) des opérateurs dans les expressions. L'analyse ascendante
quelle qu'elle soit, est toujours plus rapide que l'analyse descendante. La
précédence d'opérateurs a fait l'objet d'explications détaillées sur cet aspect au
chapitre 6.

Enfin, après avoir défini scrupuleusement les représentations intermédiaires les


plus courantes, et donné les raisons essentielles qui motivent ici le choix du
formalisme du code à trois adresses, il convient alors de donner un aperçu de ce
code, appliqué aux instructions les plus couramment utilisées par les langages de
programmation. La section 2.1 suivante sera consacrée entièrement à la génération
de la forme intermédiaire (code à trois adresses). On ne s'intéresse pas ici, à vrai
dire, aux différentes routines sémantiques qui constituent le générateur de la
forme intermédiaire, mais on est intéressé plus par les résultats qu'elles renvoient,
c'est-à-dire, la forme intermédiaire proprement dite.

2.1 Génération de la forme intermédiaire pour les expressions


Quand on parle d'expression on sous-entend toute sorte d'expression
(arithmétique, logique etc.).
Pour générer le code à trois adresses d'une expression on a le choix entre les
deux modes d'écriture présentés plus haut, à savoir, le format « Opérateur,
Opérandel, Opérande2, Résultat » ou le format «Résultat :=, Opérandel,
Opérateur, Opérande2 ». Cependant, il va falloir fixer désormais ici l'un d'eux. On
opte ici pour le premier format. Pour plus de clarté et de simplicité on s'appuie
sur des exemples concrets. Pour rappel, le champ 11 Résultat 11 est représenté par
une variable dite temporaire Ti. On l'appelle variable temporaire car elle peut
contenir un résultat intermédiaire pour le calcul d'une expression.
Une expression peut être subdivisée en plusieurs séquences de code qui utilisent
des temporaires. Quand on utilise un temporaire Ti, on crée systématiquement
une variable correspondante Ti dans la table des symboles. L'entrée dans la table
des symboles pour la variable Ti est référencée durant la phase de génération du
code cible.
Traduction 303

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

Expression logique simple


Ce type d'expression est dit simple relativement aux opérateurs fondamentaux de
l'algèbre de Boole, à savoir, OR (ou), AND (et), NOT (non)
Soit l'expression "a AND ( b OR NOT c) AND b OR c". Les variables a, b et c
sont des variables booléennes. Le code intermédiaire de l'expression est le
suivant:
(1) NOT, c, Tl
(2) OR, b, Tl, T2
(3) AND, a, T2, T3
(4) AND, T3, b, T4
(5) OR, T4, c, T5

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

Expression logique composée


On peut à présent parler d'expression logique au sens complet du terme. Dans un
tel cas, une expression logique composée est le résultat de la combinaison entre
expression relationnelle et expression logique simple telles qu'elles ont été définies
304 Chapitre 7

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.

2.2 Génération de la forme intermédiaire pour l'instruction


d'affectation
La génération de la forme intermédiaire pour une affectation ne devrait poser
particulièrement aucun problème. En effet, une affectation étant logiquement
l'assignation de la valeur d'une expression à une variable bien définie. La forme
intermédiaire concernant une expression a été étudiée ci-dessus. La génération du
code à trois adresses, pour une affectation, suit le schéma suivant :
(n) ASSIGN, Ti, id; où:
ASSIGN : est le symbole indiquant une affectation.
(n) : est le numéro du quadruplet courant représentant la traduction de
l'instruction d'affectation.
Ti : est la variable temporaire où est récupéré le résultat final du calcul de
l'expression.
Id : est la variable à laquelle est assignée la quantité contenue dans la variable
temporaire Ti.
Par exemple, l'affectation "x := a * (b + c)" a pour forme intermédiaire, la
séquence de code suivante :
(1) +, b, c, Tl
(2)*, a, Tl, T
(3) ASSIGN, T, 1 X

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

d'un tableau ou du premier élément du tableau et le déplacement par rapport à


cette adresse de base pour les autres éléments du tableau).

2.3 Génération de la forme intermédiaire pour les instructions de


contrôle
On commence par l'instruction conditionnelle de base <if-then-else>. Ensuite, on
construit progressivement certaines parmi les instructions de contrôle les plus
couramment utilisées dans les langages de programmation.
A l'occasion, on fixe certains symboles représentant les opérateurs de
branchements (conditionnels ou inconditionnels) utilisés dans les instructions de
contrôle. Le Tableau LI rassemble l'essentiel de ces symboles. La liste n'est pas
exhaustive ; on peut ajouter d'autres symboles comme les opérateurs relationnels
de comparaison, etc.
Par exemple l'opérateur TAB a pour opérandes, l'adresse de base
d'implantation d'un tableau A et le déplacement par rapport à cette adresse de
base. Les opérateurs comparatifs BG, BL, BE, BNE, etc. sont utilisés pour
enrichir, et même raccourcir un peu l'écriture du code à trois adresses. Par
exemple, BG indique un branchement lorsque Opérandel est (>) supérieur à
Opérande2. En réalité, tous ces opérateurs peuvent se ramener à l'opérateur
fondamental BZ ou BNZ. En effet, par exemple l'opérateur BG qui met en
confrontation deux quantités arithmétiques P et Q, par le quadruplet "BG, P, Q,
A", peut facilement être remplacé par les deux quadruplets suivants : 11 >, P, Q,
T" (comparaison) et 11 BZ, T, , A 11 (branchement conditionnel BZ sur l'adresse A).
De toute façon quand on utilise un nouvel opérateur pour exprimer une
condition, on doit rappeler ou donner sa signification.

Symbole ou opérateur Effet


branchement vers une certaine
BZ adresse quand une condition est égale à
faux. BZ : Branch on Zero
BR indique un branchement inconditionnel
le contraire de BZ. Le branchement a
BNZ
lieu quand la condition est vérifiée. vrai
branchement conditionnel suite à la
BG, BGE, BL, BLE, BE, BNE, etc.
comparaison de deux quantités
TAB opérateur définissant un tableau

Tableau LI - Liste de quelques symboles de branchement

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

par la syntaxe) ou un STDS (Schéma de Traduction Dirigé par la Syntaxe) qui


ont été définis et étudiés au chapitre 4. Autrement dit, on s'intéresse ici
uniquement à l'application et aux résultats des actions sémantiques et non aux
actions elles-mêmes.

Instruction conditionnelle <if-then-else>


On suppose que la syntaxe de l'instruction est : « if cond then Il else 12 ».
L'expression logique notée "cond" est supposée déjà générée, et le résultat du
calcul est stocké dans un temporaire que l'on peut noter TO.
Il est l'instruction à exécuter quand la condition est vérifiée (vrai).
12 est l'instruction à exécuter quand la condition n'est pas vérifiée (faux).
"then" est le mot clé qui dirige vers l'instruction Il quand la condition est vraie.
"else" est le mot clé qui dirige vers l'instruction 12 quand la condition n'est pas
vérifiée (faux).
"if' est le mot clé permettant d'identifier une instruction conditionnelle.
On ne rentre pas ici dans les détails concernant la forme intermédiaire des
instructions Il et 12, car ces dernières seront traduites indépendamment de la
traduction de l'instruction conditionnelle <if-then-else>.
Les mots clés if, then et else ne réapparaitront pas explicitement dans la
forme intermédiaire générée. Néanmoins, leur effet s'exprime implicitement dans
la forme intermédiaire obtenue selon la syntaxe du langage intermédiaire utilisé
(ici on utilise le code à trois adresses).

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

traduction ci-dessus. Le bloc (iv) commence par le premier quadruplet de la


séquence de quadruplets associée au bloc d'instructions 12.
Lorsque la condition BZ est égale à vrai, on passe en séquence au bloc
d'instruction Il.
BR, @adr représente un branchement inconditionnel vers l'adresse @dr qui
est le numéro du quadruplet qui suit directement la séquence de
quadruplets associée au bloc d'instructions 12.

Par exemple, soit à générer la séquence de code à trois adresses pour


l'instruction conditionnelle suivante :

if (a* b > 2) and ( c = d)


then x := x + 1
else x := x - 1

La séquence de quadruplets correspondante est la suivante :

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.

Comme mentionné ci-dessus, l'instruction conditionnelle est le pivot de toutes


les instructions de contrôle. Il suffit alors de reconduire la même démarche pour
établir le modèle de traduction pour toute autre instruction de contrôle. Ainsi, par
exemple pour l'alternative simple sélective « if cond then 1 », le choix est unaire
contrairement à « if cond then Il else 12 » où le choix est binaire. Donc, il faut
faire en sorte que la deuxième alternative, à savoir, le bloc d'instruction 12
correspondant au else, soit vide.
308 Chapitre 7

Instruction conditionnelle (l'alternative simple sélective) <if-then>


Sur la base du modèle de traduction correspondant à l'instruction <if-then-else>
on peut déduire celui de <if-then>. Ainsi, le modèle de traduction pour <if-hen>
est donné comme suit :

(i) BZ, TO, (J)


(ii) Séquence de code pour le bloc 1 {lorsque la valeur de TO :;:. O}
(iii) Séquence de code qui suit le bloc 1 {lorsque la valeur de TO = O}
(iv)

On voit bien qu'il s'agit d'une forme simplifiée du modèle de traduction associé
à l'instruction conditionnelle <if-then-else>.

TO étant la variable temporaire où est récuperee la valeur supposée de


l'expression qui est censée représenter la condition 11 cond 11 •
BZ (Branch on Zero) étant le code opérateur indiquant un branchement à
l'adresse (non encore connue) j lorsque la condition 11 cond" est égale à faux.
L'adresse j est le premier quadruplet du bloc (iii) du modèle de traduction ci-
dessus.
Lorsque la condition BZ est égale à vrai, on passe en séquence au bloc
d'instruction(s) 1.

Pour plus de clarté, on propose un exemple d'application. Soit alors à traduire,


sous forme de quadruplets, l'instruction de contrôle suivante :
if (a> 2) then x := x +1
La séquence de quadruplets correspondant conformément au modèle de
traduction précédent est la suivante :
(1) BLE, a, 2, (4)
(2) +, x, 1, Tl
(3) ASSIGN, Tl, X
(4) ...
BLE: (Branch on Less than or Equal). Se brancher à l'adresse j lorsque la quantité
a est plus petite (inférieure) ou égale à la valeur 2. L'adresse j (j = 4) comme
mentionné ci-dessus, n'est pas connue à l'avance. Cette adresse ne sera déterminée
qu'après avoir rencontré la séquence qui vient juste après l'affectation x := x + 1.

On aurait pu utiliser l'opérateur BZ (Branch on zero) au lieu de l'opérateur


BLE, mais avec un quadruplet supplémentaire. Ce qui est illustré par la séquence
alternative suivante :
(1) >, a, 2, TO
(2) BZ, TO, (5)
(3) +, x, 1, Tl
(4) ASSIGN, Tl, X
(5) ...
Traduction 309

Boucle tantque cond do 1 < do-while>


La boucle (ou la répétition conditionnelle) elle aussi, peut être facilement déduite
de ce qui précède moyennant quelques ajouts. Ces ajouts concernent la répétition
du bloc d'instructions 1, autant de fois que la condition "cond" reste vraie. Pour
garantir cela, on doit introduire un branchement inconditionnel BR arrière vers le
début de la séquence de code qui recalcule la condition "cond". On obtient ainsi le
modèle de traduction suivant :

a) BZ, TO, (J)


b) Séquence de code pour le bloc instruction 1 {lorsque la valeur de TO-::/:. O}
c) BR, @dr {quadruplet BR vient juste après avoir terminé le bloc instruction I}
d)

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 d) du modèle de traduction ci-
dessus. Le bloc d) commence par la séquence de code qui suit directement le
quadruplet BR ; BR indique un branchement inconditionnel.

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".

Par exemple, soit à générer la séquence de quadruplets de la répétition


conditionnelle suivante :
tantque (a > 10) faire a := a - 1
La séquence de quadruplets correspondante est la suivante :
(1) >, a, 10, Tl
(2) BZ, Tl, (6)
(3)-, a, 1, T2
(4) ASSIGN, T2, a
(5) BR, (1)
(6) ...
Ici aussi, on aurait pu utiliser l'opérateur BLE (BLE : Branch on Less than or
Equal), au lieu de BZ (BZ: Branch on Zero), et on aurait donc obtenu la
séquence de quadruplets ci-après composée de quatre quadruplets au lieu de cinq.

(1) BLE, a, 10, (5)


(2)-, a, 1, Tl
(3) ASSIGN, Tl, a
(4) BR, (1)
(5) ...
310 Chapitre 7

Boucle répéter 1 jusqu'à cond <repeat-until>


Avec la boucle <repeat-until>, contrairement à la boucle <do-while>, le bloc
d'instructions I s'exécute au moins une fois quelle que soit la condition. En effet,
le contrôle de l'état de la condition "cond" a lieu toujours après avoir effectué un
passage sur la séquence du bloc d'instructions I. Ainsi, on aura qu'un seul
branchement conditionnel BZ qui renvoie vers le début de la boucle qui est le
début du bloc d'instructions 1, ou bien il y aura passage en séquence vers
l'instruction suivante (sortie de boucle). Le modèle de traduction de la boucle ici
est le suivant :

a) Séquence de code du bloc I


b) Séquence de calcul de l'expression "cond" et son stockage dans TO
c) BZ, TO, (j)
d)

La séquence de code du bloc I est exécutée une première fois indépendamment


de la condition.

BZ étant le code opérateur indiquant un branchement à l'adresse j lorsque la


condition 11 cond 11 est égale à faux. L'adresse j est le premier quadruplet du
bloc a) du modèle de traduction ci-dessus. Lorsque BZ est vrai (condition mise
à 1), autrement dit, la condition est égale à vrai, il y a donc forcément passage
en séquence, c'est-à-dire on quitte la boucle.
Pour plus de clarté, on donne un exemple d'application. Soit alors à traduire la
boucle <repeat-until> suivante :
répéter
X :=X+ 1;
y:= y- 1;
jusqu'à x > y
La séquence de code intermédiaire conformément au modèle de traduction ci-
dessus est la suivante :
(1) +, x, 1, Tl
(2) ASSIGN, Tl, X
(3)-, y, 1, T2
(4) ASSIGN, T2, y
(5) >, x, y, TO
(6) BZ, TO, (1)
(7) ...
Ici aussi, on aurait pu employer un autre code opérateur à la place de BZ. On
peut utiliser BLE (Branch on Less or Equal) et bénéficier d'une ligne de code de
moins, comme avec le cas d'exemples similaires vus précédemment (5 lignes de
code au lieu de 6). Ainsi, la séquence de code sera la suivante :
(1) +, x, 1, Tl
(2) ASSIGN, Tl, X
(3)-, y, 1, T2
Traduction 311

(4) ASSIGN, T2, y


(5) BLE, x, y, (1)
(6) ...
A ce stade, on a passé en revue l'essentiel des instructions de contrôle les plus
courantes. Il reste néanmoins, quelques cas intéressants qu'il faudrait élucider,
comme l'instruction conditionnelle à choix multiple connue sous le nom de
« switch » ou « goto calculé » ou encore de « case ». On étudiera dans les
paragraphes qui vont suivre, l'instruction conditionnelle à choix multiple ou le
switch. De même, comme prévu plus haut, on reviendra sur le cas de l'utilisation
optimale des expressions logiques, à savoir, ne pas poursuivre le calcul d'une
expression logique : avec and lorsqu'on sait qu'une sous-expression est à faux
avant d'atteindre le connecteur and, et de même, avec or lorsqu'une sous-
expression est vraie avant le connecteur or). On se penchera aussi sur le calcul des
étiquettes ou adresses symboliques. On décrira la technique utilisée pour ce calcul.
Il faut également élucider la question concernant les variables indicées ou éléments
de tableaux.

Instruction de contrôle à choix multiple <switch> ou <case>


Il y a plusieurs formes d'instructions de type « case » ou « switch » selon les
langages. Mais, malgré la variété des formulations, le but est toujours le même, à
savoir, l'aiguillage (ou le branchement) vers l'adresse appropriée selon une
certaine condition ou valeur calculée. Pour mieux se situer, on se base sur une
forme simple comme l'instruction « case» qui est bien connue dans la plupart des
langages de programmation actuels.
La syntaxe générale de l'instruction « case » est la suivante :
selonque E vaut
V1: Ji
112 : I2

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

(10) BR, ( s) {branchement inconditionnel en sortie de l'instruction


selonque (15)}
(11) BE, TO, v1, (Ll) {tester si TO = v1 alors se brancher à Ll (4)}
(12) BE, TO, 112, (L2) {tester si TO = 112 alors se brancher à L2 (6)}
(13)
(14) BR, Ln {se brancher à Ln (9) quand TO -:t:- Vi pour i= 1..n-1}
(15)
Autrement dit :
On crée la séquence de code associée à l'expression E et on l'affecte à la
variable temporaire TO.
Ensuite on se branche (BR: branchement inconditionnel) à l'adresse j du bloc
(11) du modèle de traduction ci-dessus, pour tester la valeur de l'expression
qui se trouve dans TO.
Chaque test BE qui se trouve dans le bloc test commençant à (11) du modèle
de traduction renvoie vers l'adresse de l'instruction appropriée du code de I 1
ou I2,... jusqu'à In-1· Mais, si aucune des valeurs vi, 112, ···Vn-i. ne répond au test
Vi = TO (i = 1..n-1), alors il y aura nécessairement passage en séquence au
branchement inconditionnel BR, Ln qui renvoie vers le début (9) du code
correspondant à l'instruction In.
Chacune des séquences de code li, I2 1•• , In-1 et même In, est suivie du
branchement inconditionnel BR, (s) qui renvoie vers la sortie (15) de
l'instruction de contrôle selon.
Le modèle de traduction semble un peu long, mais pas complexe. Pour plus de
clarté on s'appuie sur un exemple d'application. Soit alors à traduire l'instruction
de contrôle à choix multiple <case> suivante :
case prix of
5: X:= X+ 10 j
10 : X := X+ 20 j
15: X:= X+ 30
else x := x + 50 ;
fin;

Cette instruction vérifie certaines conditions quant à la valeur de la variable


"prix". L'utilisateur est supposé avoir déjà affecté une valeur à la variable "prix".
Pour simplifier, cette dernière correspond à la variable temporaire TO du modèle
ci-dessus.
Le point-virgule avant le "else" est facultatif. Mais, pour éviter d'interférer
avec un bloc "if' (pouvant contenir un " else") on choisit systématiquement
d'omettre le point-virgule avant le "else".

En appliquant scrupuleusement le modèle de traduction décrit ci-dessus, on


obtient la séquence de code à trois adresses suivante commençant arbitrairement à
l'adresse (1) :
1) BR, (14)
2) +, x, 10, Tl
Traduction 313

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

Ainsi le même exemple d'application ci-dessus donnerait la séquence de


quadruplets suivante :
1) BNE, prix, Vt 1 (5)
2) +, x, 10, Tl
3) ASSIGN, Tl, X
4) BR, (15)
5) BNE, prix, V2, (9)
6) +, x, 20, T2
7) ASSIGN, T2, X
8) BR, (15)
9) BNE, prix, V3, (13)
10)+, x, 30, T3
ll)ASSIGN, T3, X
12)BR, (15)
13)+, x, 50, T4
14)ASSIGN, T4, X
15) ...

Il est également question de chercher la manière la plus optimale d'implanter


une instruction d'aiguillage comme <case> ou <selonque>. On vient de
constater précédemment qu'on pouvait utiliser une série de branchements
conditionnels (mais avec au plus une dizaine de cas). Quand le nombre de cas est
plus grand, on optera pour le premier modèle, à savoir celui suggéré en début de
cette section (concernant le contrôle à choix multiple <switch> ou <case>).
Il existe plusieurs façons d'implanter une instruction <case> ou <selonque>.
On peut utiliser une table dans laquelle seront stockées des couples ( Vi., Li) avec
i=l..n-1, où Vi. est la valeur à comparer à celle de E (stockée dans TO), et Li est
l'étiquette (adresse symbolique) associée au code de li. On place à la fin de la
table la valeur de l'expression E elle-même avec l'étiquette associée à l'instruction
autre ou else. Si le nombre de cas est supérieur à 10, on peut utiliser un adressage
dispersé (hachage) comme celui étudié avec la table des symboles. Les différentes
entrées dans la table (adresses) sont calculées sur la base d'une fonction de
hachage appliquée aux étiquettes associées au code de chaque instruction li.
Chaque entrée contient ( Vi., Li) correspondant à l'entrée calculée sur la base de Li.
Il existe une façon encore plus efficace de créer la table des différents
branchements. En effet, si les valeurs Vi. sont dans un petit intervalle Vmin·· Vmax, on
peut faire en sorte que l'étiquette associée à la valeur Vi, soit stockée dans l'entrée
ayant pour adresse Vi - Vrnin· Ainsi, pour réaliser l'aiguillage, on évalue E dans TO
puis on vérifie si TO est bien dans l'intervalle Vmin·. Vmax, auquel cas, on transfère le
contrôle à l'entrée d'adresse Vi - Vrnin· Il faut cependant noter qu'avec cette
approche, l'étiquette associée à l'entrée autre ou else, ne fait pas partie de la
table.

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

Retour sur la traduction des expressions booléennes


On peut parler essentiellement de deux méthodes de représentation de la valeur
d'une expression booléenne :
Avec la première, on peut utiliser 1 et 0 pour coder les constantes booléennes
vrai et faux, respectivement.
Avec la deuxième, on utilise plutôt une position ou étiquette (adresse de
branchement) atteinte lors de l'exécution d'un programme. Cette méthode se
prête particulièrement bien aux langages de programmation.
On peut ainsi, dire que la première correspond à un codage numérique, quant
à la seconde, c'est une représentation par position, car la valeur de l'expression
booléenne est représentée par une position (adresse de branchement) à atteindre
lors de l'exécution d'un programme.

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) ...

Comme indiqué auparavant, on doit rappeler ou préciser toujours le rôle de


chaque nouvel opérateur utilisé pour raccourcir la séquence de code obtenue.
En principe, l'évaluation du code associé à la sous-expression x > y ou z < s
suffit à évaluer toute l'expression ; la valeur du temporaire Tl est redondante. On
verra plus clairement avec le codage positionne! comment contourner
explicitement le problème de cette redondance.
Enfin, tel que mentionné ci-dessus, on va s'intéresser à présent au mode qui
consiste à traduire une expression logique selon la deuxième méthode, à savoir, le
mode positionne!. Ce dernier, comme prévu, s'appuie sur des adresses de
branchement représentées par des étiquettes ou des numéros de quadruplets.

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! :

Règles de production Règle sémantique de traduction


E ~El or E2 El.true := E.true ; El.false := Newlabel ;
E2.true := E.true; E2.false := E.false;
E.code:=El.codellGen(El.false ':')llE2.code
E ~El and E2 El.true := Newlabel ; El.false := E.false
E2.true := E.true; E2.false := E.false ;
E.code:=El.codel IGen(El.true ':')l IE2.code
E ~ not El El.true := E.false ; El.false := E.true ;
E.code:=El.code
E ~(El) El.true := E.true ; El.false := E.false ;
E.code:=El.code
E ~ idfl relop idf2 E.code := Gen ('relop', idfl, idf2, E.true) Il
Gen ('BR', E.false)
E ~ true E.code := Gen ('BR', E.true)
E ~ false E.code := Gen ('BR', E.false)

Tableau LII- Définition dirigée par la syntaxe pour la génération du code à


trois adresses des expressions booléennes.
318 Chapitre 7

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.

Traduction des variables indicées et tableaux


Le but ici est de donner un aperçu sur la traduction des variables indicées et
tableaux. Cette question s'intéresse à l'accès aux éléments d'un tableau. En effet,
il va falloir que les éléments du tableau soient repérés ou adressés. Donc, il faut
trouver un moyen de traduire les variables (tableaux) ainsi que leurs indices sous
forme de code à trois adresses. Mais, avant cela, on donne un bref aperçu sur le
calcul des adresses relatives des éléments d'un tableau.
Pour simplifier, on commence avec un tableau à une seule dimension, que l'on
généralisera par la suite à plusieurs dimensions. Dans ce cas, si la taille d'un
Traduction 319

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

Ici aussi, la partie constante connue à la compilation est :


C =base - ( ... (inf1 * 7i2 + inh) * n3 + inf3) ... ) * nk + infk) *taille
Pour la partie variable de l'adresse @ A [i1, 'Ï2, ... , ik] de l'élément A [i1, 'Ï2, ... , ik],
on utilise la formule de récurrence :

U1 = ii
Um = Um-1 * 7irn + im
320 Chapitre 7

Pour représenter un tableau A de dimension 5x6, c'est-à-dire ni = 5 et n:i = 6


ayant pour adresse de base d'implantation base = 1 et taille = 4, la partie
constante de l'adresse relative d'un élément A [i1, i-.2] est C = 1 - (1 * 6 + 1) * 4
= - 27. La séquence de code à trois adresses générée pour le calcul de l'adresse
relative de l'élément A [ii, i-.2] est la suivante :

(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

L'opérateur TAB est équivalent à l'opération T2 [T3] qui permet d'ajouter la


partie variable à la partie constante conformément à la formule de calcul d'adresse
de l'élément A [i1, i-.2].

Outils utilisés pour générer le code intermédiaire


Les outils couramment utilisés pour générer le code intermédiaire sont la
DDS (Définition Dirigée par la Syntaxe) ou le STDS (Schéma de Traduction
Dirigée par la Syntaxe). Une description assez claire de ces deux outils a été
donnée au chapitre 4

Le déclenchement des routines de traduction (règles sémantiques ou actions


sémantiques) est une opération indépendante du modèle de forme intermédiaire
(forme post-fixée, code à trois adresses ou arbre abstrait) que l'on voudrait
générer. L'idée est de faire abstraction de la forme intermédiaire projetée et
d'utiliser ou de construire un traducteur qui produit la forme intermédiaire
désirée. On doit alors préciser le type de forme intermédiaire que l'on voudrait
produire en sortie. Il suffit pour cela de déclencher la procédure adéquate pour la
circonstance. Il sera ainsi aisé de produire la forme intermédiaire choisie.

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

Une autre procédure ou fonction couramment utilisée au cours de la génération


de code est celle qui a pour rôle de créer les variables temporaires utilisées par les
quadruplets. On peut utiliser pour cela une procédure comme celle dénommée
NEWTEMP suivante :
procédure NEWTEMP (i, T);
début
if-i+l;
T f- '$' 11 'i'
fin;
Les temporaires seront $1, $2, ... $k ... , si le compteur i est initialisé à 0 (i f- 0).
Les variables i et T sont des paramètres formels (variables). De même, en ce qui
concerne op (opérateur), argl, arg2 et T (opérandes). L'opération de
concaténation '$' 11 'i' dépend du langage d'implémentation du traducteur.
Les autres formes intermédiaires comme l'arbre abstrait ou le code post-fixé,
ne nécessitent pas de variables temporaires comme celles utilisées par les
quadruplets. Le code post-fixé et l'arbre abstrait ont été décrits tout-à-fait en
début de section 2 de ce chapitre. Le code post-fixé est exploité efficacement en
utilisant une pile. En effet, un algorithme utilisant une pile simule le
comportement d'une machine à pile, soit pour interpréter (exécuter) directement
le code post-fixé, soit pour produire un code machine qui sera à nouveau compilé
(dans la plupart du temps, il s'agit d'un assemblage et non d'une compilation au
sens classique du terme). Une esquisse de ce genre d'algorithme est présentée dans
la partie immédiatement avant la « Remarque 2.2 » de la présente section.
Quant au code basé sur l'arbre abstrait, tel qu'on l'avait qualifié précédemment, il
représente la forme intermédiaire la plus générale, et peut, de ce fait, être exploité
sur n'importe quelle machine et avec n'importe quel langage. Il suffit alors
d'associer l'algorithme approprié pour son exploitation. On peut même en déduire
d'autres formes intermédiaires comme le code à trois adresses, la forme post-fixée,
etc. En effet, on peut par exemple explorer l'arbre selon un parcours en
profondeur post-ordre si on veut obtenir la forme post-fixée, et avec un autre
parcours en profondeur si on veut produire le code à trois adresses équivalent, etc.
Enfin, tout comme avec les deux autres formes intermédiaires (code à trois
adresses et forme post-fixée), l'arbre abstrait, lui également, permet de poursuivre
le processus de compilation avec l'une ou l'autre des deux options (compilation ou
interprétation), à savoir, pousser le processus de compilation jusqu'au bout, c'est-
à-dire générer du code machine qui sera à nouveau compilé, ou bien se contenter
tout simplement d'interpréter directement la forme intermédiaire.
En complément au volet concernant les outils impliqués, il convient d'ajouter
un aperçu sur le contrôle et le calcul des types des entités manipulées dans un
programme informatique.

Routines sémantiques de vérification et calcul de types


Un aspect important et caractérisant le travail d'un compilateur est la cohérence
entre le programme source qui se présente à son entrée et le programme
(équivalent) généré à sa sortie. Cette cohérence est garantie par le respect :
322 Chapitre 7

de la syntaxe du langage d'entrée ainsi que celle du langage de sortie ;


de la sémantique véhiculée par les instructions en entrée qui doit ressortir
fidèlement dans les constructions du langage de sortie.
L'analyseur sémantique s'appuie sur un ensemble de règles ou d'actions à
respecter, qui sont appliquées parallèlement à l'analyse syntaxique pour s'assurer
du bon déroulement des opérations de traduction. Une opération clé qui
caractérise l'analyse sémantique est le contrôle de type. En effet, le traducteur
(compilateur ou l'interpréteur) doit interdire des opérations non conformes aux
spécifications du langage, comme par exemple :

l'affectation d'une variable de type caractère ou type pointeur à une variable


numérique;

l'utilisation d'une variable non initialisée ou indéfinie dans une expression ;

l'utilisation de boucle non conforme aux bornes supérieure et/ou inférieure


requises,

l'utilisation d'un nombre réel pour indicer un tableau ;

un appel de procédure ou fonction avec des paramètres effectifs (réels)


incompatibles avec les paramètres formels ...

Toutefois, la spécification du langage peut permettre certaines coercitions


d'opérandes, par exemple quand un opérateur arithmétique est appliqué à un
entier et un réel. Dans un tel cas, le compilateur peut avoir en général à convertir
l'entier en réel. Par exemple, si on a la règle qui représente une
opération arithmétique comme suit :
E ~El 0 E2,
on peut calculer ou convertir, si nécessaire, le type en appliquant une règle ou une
action sémantique selon les différents cas qui se présentent :
Si le type de El est entier et le type de E2 est entier
Alors le résultat E de l'opération El 0 E2 est entier
/ * aucune conversion n'est nécessaire et le résultat E de l'opération E2 0 E2
est entier * /
Si le type de El est entier et le type de E2 est réel
Alors le résultat E de l'opération El 0 E2 est réel
/ * on convertit El en réel et le résultat E de l'opération El 0 E2 est réel */
Si le type de El est réel et le type de E2 est entier
Alors le résultat E de l'opération El 0 E2 est réel
/ * on convertit E2 en réel et le résultat E de l'opération El 0 E2 est réel */
Si le type de El est réel et le type de E2 est réel
Alors le résultat E de l'opération El 0 E2 est réel
Traduction 323

/ * aucune conversion n'est nécessaire et le résultat E de l'opération El 0 E2


est réel*/

De même, si on a la règle suivante qui représente l'affectation :


1---+ idf := E
on se ramène toujours au type adéquat (ici réel). Donc, on aura ce qui suit :
Si E est entier et idf est réel
Alors E devient (par conversion) réel
/ * Convertir E en réel et l'affecter à idf qui est réel * /

Si E est réel et idf est entier


Alors idf devient (par coercition) réel
/ * Considérer idf comme réel (coercition) et lui affecter E qui est réel * /

On peut rencontrer dans différentes parties d'un programme ce genre de


contrôle. On peut, en l'occurrence, citer la non-conformité des indices réels d'un
tableau ou le cas d'appel de procédure avec des paramètres effectifs incompatibles
avec les paramètres formels. Pour un tableau, on peut agir par coercition en
transformant les indices de type réel en type entier, tout comme il est possible de
rejeter carrément les indices réels et attendre que l'utilisateur rectifie le type de
ces indices.

En ce qui concerne l'incompatibilité des paramètres effectifs avec les paramètres


formels d'une procédure (ou fonction), il est possible de passer par coercition du
type réel au type entier et vice versa, mais le compilateur peut aussi rendre le
contrôle à l'utilisateur pour remédier aux types considérés comme erronés ou
inconsistants ; cela peut arriver surtout quand les types des paramètres effectifs
sont complètement incompatibles avec les types des paramètres formels comme,
par exemple, une correspondance (numérique/chaine, ou chaine/booléen, etc.)

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 :

de maitriser la syntaxe et la sémantique de la forme intermédiaire générée ;


de bien connaitre les caractéristiques d'une machine (réelle ou hypothétique)
cible sur laquelle on est censé exécuter le code final obtenu.
324 Chapitre 7

3 Génération de code machine cible


La génération de code cible telle qu'on l'avait annoncé tout à fait au début de cet
ouvrage, est la phase finale du processus de compilation. Elle diffère selon les
compilateurs. On distingue deux sortes de langages cibles :
les langages machine (comme l'assembleur) dont les instructions peuvent être
directement exécutés par des composants matériels (des machines concrètes) ;

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.

3.1 Conception d'un générateur de code cible


La partie finale, telle qu'on l'avait déjà qualifiée, constitue la synthèse du
compilateur, c'est-à-dire la production du code cible. On rappelle que cette partie
dépend du langage intermédiaire (qui a permis d'écrire le code intermédiaire) et
des caractéristiques de la machine cible (pour produire le code cible). Au-delà de
ces détails d'implantation, d'autres problèmes communs à tous les générateurs de
code, concernent la gestion de la mémoire, l'allocation des registres, l'ordre
d'évaluation, etc.
La conception d'un générateur de code cible nécessite de satisfaire, au
préalable, certaines conditions, qui sont explicitées dans les points suivants :
Disposer d'une forme intermédiaire (post-fixée, code à trois adresses, arbre
abstrait, etc.), associée avec la table des symboles. Cette dernière permet le
calcul des adresses à l'exécution des données représentées par des noms de
variables de la forme intermédiaire. On suppose, en particulier, que les noms
de ces variables puissent être représentés par des objets directement
manipulables par la machine cible (bits, réels, entiers, etc.). On estime
également que la vérification de type (à l'image de celle décrite ci-dessus) a
Traduction 325

é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é.

Comme mentionné ci-dessus, sur la base de l'information stockée dans la table


des symboles, on peut associer à un nom une adresse relative dans la zone de
données d'une procédure du programme source. Ainsi, à la génération du code
machine, on doit convertir par exemple les étiquettes (ou numéros) des
instructions du code à trois adresses en adresses d'instructions. Si on rencontre
une instruction de branchement comme (j) BR, (i), ou (j) BZ, (i), ou autre,
dans la forme intermédiaire, on doit transformer les étiquettes (ou numéros) de
quadruplets (j) et (i) correspondants. Donc, si le numéro (i) est plus petit que
le numéro (j), on génère une instruction de branchement dont l'adresse cible
correspond à l'adresse mémoire de la première instruction du code associé au
quadruplet numéro (i). Si le numéro (i) est plus grand que (j), on produit un
branchement avant (en laissant l'adresse cible à blanc) ; ensuite on range dans
une structure associée au quadruplet (i), l'adresse de la première instruction
machine associée au quadruplet (j). A la rencontre du quadruplet numéro (i),
on met à jour l'adresse cible de toutes les instructions qui font référence à (i)
(qui sont des branchements avant vers le numéro (i)). C'est exactement la
même technique qui est appliquée avec les quadruplets quand on effectue un
branchement vers un quadruplet dont on ne connait pas encore son numéro.

Si on ne cherche pas à produire un code cible efficace, la sélection des


instructions sera simple et ne posera aucun problème. Par exemple, la séquence
de code à trois adresses "+, a, b, c" sera tout simplement traduite en code
cible comme suit :

MOV, a, Ri / * charger a dans le registre Ri * /


ADD, b, Ri /*ajouter b à Ri*/
326 Chapitre 7

MOV, Ri, c / * ranger le contenu de Ri dans c */


Cette manière de produire du code instruction par instruction n'est
malheureusement pas toujours optimale, et conduit souvent à du code de
mauvaise qualité. En effet, par exemple, si on est appelé à traduire en code
cible, la séquence de code à trois adresses suivante :
(1) +, x, y, z
(2) +, z, w, t
on obtiendrait la traduction suivante :

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

L'allocation des registres joue également un rôle prépondérant dans la qualité


du code produit. Il convient alors d'utiliser les registres de manière optimale. Il
faut noter que les instructions ayant des registres comme opérandes sont
généralement plus rapides que les instructions allant chercher leurs opérandes
en mémoire. On peut diviser l'utilisation des registres en deux catégories :
• La première catégorie concerne l'allocation des registres. Pendant
l'allocation des registres, on choisit l'ensemble des variables qui vont
résider dans des registres en un point donné du programme.
• La deuxième catégorie concerne l'assignation des registres. Pendant la
phase d'assignation des registres, on choisit le registre dans lequel une
variable donnée va résider.
Traduction 327

Le choix de l'ordre d'évaluation, lui aussi, joue un rôle prépondérant dans la


qualité et l'efficacité du code cible généré. En effet, certains ordres de calcul
utilisent moins de registres que d'autres.
A cette issue, on peut dire qu'on a à peine abordé les points essentiels
concernant les problèmes posés par la conception d'un générateur de code cible.
Le processus est encore long et onéreux, et demande par ailleurs d'introduire au
préalable de nouveaux concepts, on pense particulièrement à l'optimisation du
code, l'allocation de la mémoire, etc.
Avant de clore cette partie, on donne un bref aperçu sur la génération de code
cible, moyennant quelques exemples.

3.2 Aperçu sur le processus de génération de code cible


On utilise une machine hypothétique définie par un ensemble d'instructions
assembleur. On ne donne ici qu'une partie du jeu d'instructions que l'on utilise
uniquement pour le besoin des exemples proposés :
SKP / * sauter la prochaine instruction * /
JMP / * se brancher à l'adresse indiquée * /
LDR / * charger (placer) une valeur dans un registre */
STM / * stocker (placer) une valeur dans une cellule mémoire */
ADD / * addition de deux valeurs * /
SUB / * soustraire une valeur d'une autre * /
MLT / * multiplier deux valeurs * /
DIV / * diviser une valeur par une autre * /
Les deux instructions ayant respectivement pour codes SKP et JMP sont des
branchements inconditionnels. On peut obtenir leurs homologues conditionnels,
SKPLE (LE : Less or Equal), JMPLE, SKPNE (NE : Not Equal), JMPNE, etc.,
en leur concaténant les opérateurs relationnels (LT, LE, NE, EQ, GE, GT). On
peut aussi avoir JMPT ou JMPF qui sont des branchements conditionnels avec la
valeur TRUE ou F ALSE dans un registre.
Une instruction de cette machine suit le format suivant :
<code-opération>, <numéro de registre>, <adresse>
où une adresse est, soit un <numéro>, soit <numéro> (<registre>).
Par exemple :
JMPF, 1, 32
ADD 1, 4327 (5)
Il est préférable de scinder les instructions en différents groupes par le biais
d'une lettre minuscule comme suffixe à la fin du code opération de l'instruction.
Cela permet logiquement d'avoir différentes opérations, qui sur de nombreuses
machines réelles sont mises en œuvre par des instructions très différentes,
328 Chapitre 7

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.

Soit alors à générer le code cible de l'instruction conditionnelle suivante :


if (y* 10 + x = 125) or m then <statement>
Il est relativement facile de traduire (générer du code) pour une phrase si on
sait comment générer le code pour ses sous-phrases. On présente ci-après de
simples procédures (routines sémantiques) qui traduisent de cette manière
[Bornat, 86].
i. <Instruction conditionnelle if>
Appel de la procédure qui genere le code de l'expression logique et
placement de sa valeur dans le registre 1
Traduction 329

- NewLabel ($Ef) : Créer une étiquette $Ef


- Générer JMPF 1, $Ef
- Appel de la procédure qui génère le code du bloc <statement>
- Générer l'étiquette $Ef :

ii. <Opérateur or >


Appel de la procédure qui genere le code de l'opérande gauche de
l'opérateur or et placement de sa valeur dans le registre i
- Appel de la procédure qui génère le code de l'opérande droit de l'opérateur
or et placement de sa valeur dans le registre i + 1
Générer ORr i, i+1

iii. <Opérateur relationnel = >


- Appel de la procédure qui génère le code de l'opérande gauche de la
relation =, et placement de sa valeur dans le registre i + 1
- Appel de la procédure qui génère le code de l'opérande droit de la relation
=, et placement de sa valeur dans le registre i + 2
- Générer:
LDRn i, TRUE
SKPEQr i + 1, i+2
LDRn i, F ALSE

iv. <Opérateur + >


Exactement comme l'opérateur or, excepté la dernière instruction qui est
remplacée par l'instruction suivante :
ADDr i, i+1

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

La production de code cible est réalisée en parcourant un arbre abstrait de


l'expression. Les procédures précédentes concernent les opérateurs, donc les nœuds
internes de l'arbre abstrait. Les procédures suivantes concernent les nœuds feuilles
(nœuds externes) de l'arbre abstrait de l'expression.
vi. <nom>
Générer LDR i, <adresse dénotée par nom>

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>

$Ef : / * étiquette qui indique le reste du programme */


<suite du programme>
Remarque 3.2
La première instruction de la partie désignée comme étant la suite du programme,
est précédée de l'étiquette $Ef. Cette dernière constitue l'adresse de branchement
lorsque l'instruction conditionnelle considérée est égale à faux.
L'amélioration (optimisation) du code basé sur le parcours d'arbre, suppose
l'application d'une ou plusieurs des possibilités résumées comme suit :
Générer du code spécial pour un nœud avec une propriété locale spéciale (qui
est une tactique).
Modifier la façon dont l'arbre est parcouru quand un nœud à une propriété
générale singulière (qui est une stratégie).
Modifier l'arbre avant qu'il soit parcouru (c'est une optimisation de l'arbre).
Modifier le code généré après la fin du parcours de l'arbre (qui est une
optimisation du code).
L'amélioration de la procédure de génération de code de
l'opérateur + s'appuie sur une manœuvre tactique (comme la première des
possibilités relatées ci-dessus). En utilisant cette procédure, les trois premières
instructions du code se ramènent à deux et, si une amélioration similaire est
appliquée aux procédures qui traduisent les opérateurs = et or, le traducteur
produit le code (listé précédemment) de l'exemple proposé. Le code obtenu est
Traduction 331

une amélioration considérable par rapport à celui obtenu précédemment - Il


utilise deux registres, plutôt que trois, et sept instructions plutôt que douze - et
il a été obtenu par des changements simples et modulaires dans les procédures du
traducteur.
La procédure concernant l'opérateur '+' a été modifiée comme suit :
<Opérateur + >
Générer du code calculant la valeur de l'opérande gauche à placer dans le
registre i
Si l'opérande droit est un nom
Alors Générer ADD i, <adresse de la variable nom>
Sinon
Si l'opérande droit est un nombre
Alors Générer ADDn i, <valeur du nombre>
ou ADD i, <place où se trouve la valeur de nombre>
Sinon
Générer du code calculant la valeur de l'opérande droit à placer dans
le registre i + 1
Générer ADDr i, i+ 1

Le code obtenu après l'application des procédures remaniées selon la tactique


présentée ci-dessus, est le suivant :
LDR 2, y
MLTn 2, 10
ADD 2, X
LDRn 1, TRUE
SKPEQn 2 125
LDRn 1, FALSE
OR 1, m

A présent, si on suppose que le code intermédiaire consiste en une


représentation post-fixée, on peut lui associer un générateur de code cible basé sur
un algorithme itératif comme suit :
i +-- 1 ; / * le prochain registre libre est indiqué par i */
tantque non-fin de code-post-fixé
faire selonque entité est
identificateur : Générer (LDR i, <adresse-variable>)
i+-i+l;
nombre: Si nombre est court
Alors Générer (LDRn, i, <valeur-nombre>)
Sinon Générer (LDR, i, <adresse-nombre>)
i+-i+l;
'+' : Générer (ADDr, i - 2, i - 1)
i+-i-1;
'*' . Générer (MLTr, i - 2, i - 1)
332 Chapitre 7

i+-i-1;

·-·. Générer (LDRn, i, TRUE)


Générer (SKPEQr, i - 1, i - 2)
Générer (LDRn, i, F ALSE)
Générer (LDRr, i - 2, i)
i+-i-1;
'OR': Générer (ORr, i - 2, i - 1)
i+-i-1;
'Autrement' : Erreur ( ' ' ) ;
entité +--- entité suivante
fait;
Le code généré après l'application de cet algorithme sur le même exemple est le
suivant:
LDR 1, y
LDRn 2, 10
MLTr 1, 2
LDR 2, X
ADDr 1, 2
LDRn 2, 125
LDRn 3, TRUE
SKPEQr 1, 2
LDRn 1, FALSE
LDRr 1, 3
LDR 2, m
ORr 1, 2

L'algorithme itératif ci-dessus (ayant pour donnée une représentation post-


fixée) délivre le même nombre d'instructions (douze) que l'algorithme qui
parcourt l'arbre abstrait (sans remaniement des procédures qui traitent les
opérateurs+, OR et=). Ceci sous-entend qu'il est important de chercher toujours
à obtenir un code plus performant, c'est-à-dire qui soit de taille réduite et sans
redondance. A cet effet, il faut appliquer une ou plusieurs des possibilités
d'optimisation recensées précédemment pour améliorer la qualité du code produit.
Dans ce contexte, si on considère l'expression arithmétique x + y * 10, ayant pour
arbre abstrait celui de la Figure 132, on peut générer le code cible en appliquant
de manière naïve les procédures adéquates sans se soucier de la qualité du code
produit, et on peut également générer le code cible, tout en améliorant les
procédures de traduction particulièrement celles qui traitent les opérateurs +, OR
et =. Ainsi, le code obtenu (sans modification des procédures) est le suivant :
LDR 1, X
LDR 2, y
LDRn 3, 10
MLTr 2, 3
ADDr 1, 2
Traduction 333

+
/ "*
"
X

y/ 10

Figure 132: Arbre abstrait de l'expression x +y* 10

Le code obtenu après modification des procédures concernées est le suivant :


LDR 1, y
MLTn 1, 10
ADD 1, X

L'exemple x + y * 10 peut apparaitre dans n'importe quelle partie d'un


programme ; il peut même apparaitre plusieurs fois. En comparant les tailles
respectives des deux codes, on peut apprécier l'amélioration apportée. En effet, la
taille a été réduite quasiment de 503, ce qui est substantiellement une très bonne
performance, notamment quand on passe à l'échelle réelle des programmes
informatiques.

Avant de clore le sujet on ajoute quelques procédures de traduction. Pour


montrer comment on a généré les deux séquences de codes ci-dessus concernant
l'exemple x + y * 10, il convient de présenter les routines de traduction des
expressions arithmétiques, ainsi que leurs alternatives remaniées pour optimiser le
code. Pour rappel, ces procédures sont appliquées en parcourant l'arbre abstrait.

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) ;

identificateur : Générer (LDR, i, n.adresse) ;


nombre: Générer (LDRn, i, n.valeur) ;
'Autrement' : Erreur (' ')
334 Chapitre 7

En appliquant scrupuleusement les deux procédures récursives définies ci-


dessus sur l'arbre abstrait (Figure 132) de l'expression x +y* 10, on aboutit au
premier code que l'on qualifie de code non optimal, car n'ayant apporté aucune
modification pour améliorer la procédure OperateurBin.
Ci-après sont décrites alors les procédures remaniées en vue d'une optimisation
du code comme prévu.
Procédure OperateurBin (op, n, i)
début
first ~ n.filsgauche ;
second ~ n.filsdroit ;
Si Feuille (first) et non Feuille (second)
/ * permuter et inverser l'opération * /
Alors first ~ n.filsdroit ;
second ~ n.filsgauche ;
op ~ inverser (op) / * changer op en xop (inversion) */

ExpArithmétique (first, i) ; / *appel à la procédure ExpArithmétique * /


Si Feuille (second)
Alors TraitFeuille (op, second, i)
Sinon 1 ExpArithmétique (second, i + 1) ;
Générer (opll'r', i, i + 1)
fin

Fonction Feuille (n) : booléen ;


début
Feuille~ (n.type = identificateur) ou (n.type =nombre)
fin

Procédure TraitFeuille (op, n, i)


début
selonque n.type est
identificateur : Générer (op, i, n.adresse) ;
nombre: Générer (opll'n', i, n.valeur) ;
'autrement' : Erreur (' ')
fin

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

Il est bien établi aujourd'hui que les progrès accomplis en matière de


techniques de compilation sont considérables et incontestables. A titre indicatif,
une part importante de l'effort intellectuel consacré traditionnellement à la partie
frontale d'un compilateur peut être confiée aujourd'hui à des outils générateurs
d'analyseurs comme Lex (Flex) pour l'analyse lexicale et Yacc (Bison) pour
l'analyse syntaxique. Par ailleurs, l'émergence d'outils de développement comme
les AGL, a transformé littéralement la manière dont on produit le logiciel en
général, et a propulsé la production de compilateurs du stade artisanal au stade
industriel.

Pédagogiquement parlant, l'ouvrage synthétise l'essentiel des méthodes (outils


et techniques) issues de la théorie de la modélisation syntaxique (grammaires,
automates, diagrammes et graphes syntaxiques, etc.). Il donne également en
parallèle un aperçu de l'application de ces méthodes sur des exemples illustratifs
et adaptés. On espère qu'il sera à la hauteur de l'espérance du lecteur, et lui
permettra de comprendre ce qu'il y a derrière les compilateurs et traducteurs.
Des séries d'exercices avec leurs corrigés ont été ajoutées à la fin des trois
premiers chapitres. Un exercice récapitulatif qui rappelle les différentes approches
et méthodes d'analyse syntaxique a été également ajouté à la fin du chapitre
6. On aurait voulu faire de même pour les chapitres 5 et 7, mais cela n'a pas été
possible. En effet, ces derniers nécessitent plutôt des exercices pratiques, voire des
TP (travaux pratiques) qu'il n'est pas aisé de rapporter dans un nombre limité de
pages.
Enfin, on estime que l'ouvrage est clair de par l'aspect pédagogique de son
exposé et le nombre considérable de schémas (figures et tableaux) qui y sont
ajoutés pour l'enrichissement du discours et l'illustration des concepts étudiés.
Bibliographie

[Aho, 86] A. Aho, R. Sethi and J. Ullman, « Compilers: principles, techniques,


and tools » Addison-Wesley Longman Publishing Co., Inc. Boston, MA, USA
@1986 ISBN : 0-201-10088-6, 1986.

[Aho, 73] A. Aho and J. Ulmann « The theory of Parsing, Translation and
Compiling » (Vl: Parsing, V2: Compiling), Prentice Hall, 1973.

[Bornat, 86] R. Bornat, « Understanding and Writing Compilers » MACMILAN


EDUCATION, ISBN 0-333-21732-2. Printed in Hong Kong, 1986.

[Floyd, 63] R. W. Floyd, « Syntactic analysis and operator precedence », J.


ACM 10 : 3, 316-333, 1963.

[Gallaire, 84] H. Gallaire, « Techniques de compilation », CEP ADUES, 1984.

[Knuth, 65] D. E. Knuth, « On the translation of languages from left to right »,


lnform. and Control 8 : 12, 735-736, 1965.

[Lesk, 75] M.E. Lesk, «Lex - A Lexical Analyzer Generator », Camp. Sei.
Tech. Rep. No. 39 (Murray Hill, New Jersey : Bell Laboratories), 1975.

[Myriam, 91] N. Myriam, « Théorie des langages», OPU (Office des


Publications Universitaires), Alger, 1991.
Table des figures

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

75 Parcours de l'arbre abstrait de l'expression 11 a * b + c 11 149


76 Parcours en pré-ordre de l'arbre abstrait de l'expression 11 a * a 11 150
77 Arbre syntaxique de l'expression 11 6 - 1 + 7 11 151
78 Arbre syntaxique annoté et décoré de l'expression 11 6 - 1 + 7 11 151
79 Algorithme général de parcours en profondeur d'un arbre 152
80 Calcul des attributs par application des règles sémantiques au cours du
parcours en profondeur : cas de l'expression 11 6 - 1 + 7 11 152
81 Arbre syntaxique d'un schéma de traduction 153
82 Application des règles sémantiques traduisant l'expression 11 a*(a+a) 11 en
11 a a a+ * 11 154
83 Application des actions sémantiques traduisant l'expression 11 a *(a+ a) en
11 a a a+ * 11 155

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

par chainage interne 214


112 Table des symboles basée sur l'accès dispersé ; résolution des collisions
par chainage externe 214
113 Exemple de table des symboles basée sur l'accès dispersé ; résolution des
collisions par rehachage et chainage interne 215
114 Stockage des caractères qui forment un identificateur dans un tableau
séparé nommé table des noms 217
115 Table des symboles par niveaux et par blocs 218
116 Table des symboles en pile 219
117 Tables des symboles pour des procédures imbriquées 220

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

positions des nœuds dans l'arbre de l'expression (alb) *bbalc +# 204


XXVII Transitions à partir des positions 1, 2, 3 et 6 exprimées à l'aide de la
fonction follow dans l'arbre de l'expression (alb) *bbalc +# 205
XXVIII Règles de calcul des fonctions nullable, firstpos et lastpos 207
XXIX Résultats obtenus en appliquant les fonctions nullable, firstpos et
Lastpos sur l'expression régulière (alb)*bbalc+ # 207
XXX Stockage des caractères qui forment un identificateur dans une zone
de taille fixe de son entrée 217

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

XLIX Automate d'états d'entrée dans les expressions arithmétiques 283

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

Automate à pile déterministe ........ 233


A Automate généralisé ....................... 41
Acceptation par pile vide ............... 92 Automate minimisé ........................ 46
Accès dichotomique ...................... 209 Automate miroir ............................ 54
Accès linéaire ou séquentiel.. ........ 208 Automate partiellement généralisé. 39
Accès par adressage dispersé ........ 211 Automate simple ............................ 38
Accès par arbre binaire ordonné ... 210 Axiome ...................................... 51, 78
Action sémantique ........................ 152
B
ADD ............................................. 328
ADDa ........................................... 328 Backus Naur Form ......................... 78
ADDn ........................................... 328 BE ................................................. 305
ADDr ........................................... 328 BG ........................................ 305, 315
ADDs ........................................... 328 Bison ............................. 142, 196, 335
Adresse relative ............................ 319 BL ................................................ 305
Adresse symbolique ...................... 316 BLE .............................................. 305
AFD ............................................... 42 BNE .............................................. 305
AFN ............................................... 42 BNF et EBNF ......................... 78, 145
AGL ...................................... 126, 335 BNZ .............................................. 303
Aiguillage .............................. 298, 301 Boole ............................................. 303
Algorithme de parcours de graphe 242 Bootstrap ...................................... 125
Allocation des registres ................. 133 Bottom-up ..................................... 223
Alphabet .......................................... 3 BR ....................................... 139, 294
Alphabet auxiliaire ......................... 89 Branch on Zero ............................. 305
Alphabet de pile ............................. 84 Buffer ............................................ 167
Alphabet terminal .......................... 34 BZ ......................................... 113, 294
Alphabets d'entrée et de sortie ..... 109
Alternative ..................................... 21
Alternative simple selective .......... 293 c
Ambiguë ......................................... 16 Chainage externe .......................... 213
Analyse lexicale ............................ 130 Chainage interne ........................... 213
Analyse montante .......................... 13 Chaine vide ................................ 4, 19
Analyse sémantique ...................... 130 Chomsky .................................... 7, 31
Analyse syntaxique .................. 10, 132 Classe d'équivalence ....................... 46
Appel récursif non terminal.. .......... 23 Code à trois adresses ............. 132, 297
Arbre abstrait.. ............................. 297 Code cible ............................. 126, 289
Arbre de dérivation ........................ 33 Code intermédiaire ................ 130, 303
Arden ............................................. 50 Code machine absolu .................... 128
Assemblage ................................... 124 Code machine translatable .... 128, 289
Associativité des opérateurs ......... 147
345

Code post-fixé ....................... 133, 289 Etat redondant ............................... 46


Collection canonique ............. 260, 271 Etat superflu .................................. 46
Collision ....................................... 212 Etats finals .............................. 44, 179
Compilateur multi-passe .............. 159 Etats inutiles .................................. 46
Compilateur simple-passe ............. 159 Expression booléenne ............ 114, 328
Compilateurs de silicium .............. 128 Expression logique ........ 165, 294, 316
Complexités .................................. 179 Expression logique composée ........ 304
Constantes booléennes .................. 162 Expression logique simple ..... 294, 304
Constantes numériques ................ 172 Expression relationnelle ........ 114, 303
Construction de Thompson ............ 60 Expressions arithmétiques simples. 77
Contexte libre ...................... 8, 15, 92 Expressions régulières ......... 9, 33, 144
Contrôle de type .......................... 320
Coroutine ...................... 141, 159, 220 F
Critère de décision ........................ 224 Factorisation ................... 79, 105, 230
F ALSE .................................. 327, 332
D
Firstk·············································226
D.E Knuth ................................... 238 Firstpos ................................. 204, 208
DDS .............................. 307, 320, 323 Flex ............................... 142, 196, 335
Décaler/Réduire ...................... 13, 251 Floating-point ............................... 328
Default ......................................... 180 Flot d'entrée ......................... 134, 285
Définition régulière ................ 169, 199 Floyd ............................................. 235
Dénombrable .................................... 4 Flux de tokens .............................. 159
Dérivation .................................. 8, 14 FNC .......................................... 22, 31
Dérivation canonique ............... 11, 113 FNG ............................................... 22
Dérivation directe ............................. 8 Followk··········································227
Dérivation gauche .................... 11, 95 Followpos ...................................... 204
Dérivation indirecte .......................... 8 Fonction de transition ............. 84, 102
Dérivée d'un langage ...................... 57 Forme Polonaise inverse ........ 132, 290
Descente récursive ........................ 241 Forme préfixée ...................... 148, 297
Déterminisation d'un automate fini42 Fortran .............................. 3, 156, 171
Déterminisme .......................... 94, 105
Dérivations les plus à gauche ....... 232 G
Diagramme de transition. 36, 108, 185 GE ................................................ 327
Diagramme syntaxique ............. 10, 82 Génie logiciel... ...................... 124, 142
DIV .............................................. 327 Gestion de l'espace mémoire ......... 293
Gestionnaire d'erreurs ................... 282
E GOTO ........................... 139, 272, 298
Editeurs syntaxiques .................... 126 GPO ...................................... 235, 250
Elément de fond de pile ........... 84, 95 Grammaire acyclique ..................... 21
Entités lexicales ....................... 172, 85 Grammaire ambigüe ...................... 147
Environnement du compilateur .... 134 Grammaire augmentée .......... 238, 265
EOF ...................................... 167, 179 Grammaire GR miroir .................... 55
Epsilon Free First ........................ 238 Grammaire LL .............................. 233
EQ ................................................ 327 Grammaire SLR .................... 259, 266
Erreur lexicale ....................... 178, 221 Grammaires cycliques ............. 20, 225
Erreurs secondaires ....................... 278 Grammaires non-récursives ........... 225
Erreurs syntaxiques ............... 253, 283 Grammaires régulières ............... 33, 51
346 Index

Graphe syntaxique .............. 9, 82, 242 LDR ...................................... 327, 333


Greibach ................................... 22, 25 LDRn .................................... 329, 333
GT ................................................ 327 LDRr ............................................. 332
Lookahed LR ................................. 259
H LR ................................ 6, 55, 94, 275
Heuristique ............................ 182, 224 LT ................................................. 327
Hiérarchie des grammaires ........... 264
M
I Machine à pile ....................... 133, 321
Identificateur .................. 77, 115, 160, Machine à registres ....... 132, 289, 303
Interprètes de requêtes ................. 128 Machine abstraite .............. 34, 84, 289
Item ....................................... 239, 268 Machine cible ................ 125, 144, 289
Itération réflexive et positive .......... 49 Machine hypothétique ........... 137, 327
Machine virtuelle .......................... 142
J Mémoire du compilateur ....... 208, 284
Minimisation du nombre d'états .... 42
Jan tukasiewicz ........................... 290
Mode correction globale ................ 277
JMP ............................................. 327
Mode direct ................................... 160
JMPF ........................................... 327
Mode indirect ................................ 160
JMPLT ......................................... 327
Mode panique ........................ 221, 222
JMPNE ........................................ 327
Mode syntagme ............................. 277
JMPT ........................................... 327
Modèle d'analyseur ascendant ... 95, 99
K Modèle d'analyseur descendant ...... 99
Monoïde ........................................... 4
K-inséparables ................................ 46 Mot ..................................... 3, 84, 100
K-préfixe de l'élément .................. 226 MLT .............................................. 327
K-suivant ..................................... 227 MLTn ............................................ 331
MLTr ............................................ 330
L
LALR ............................ 259, 269, 272 N
Langage ................................ 3, 15, 38 NE ................................................. 327
Langage d'assemblage ............ 289, 324 NEGr ............................................ 328
Langage intermédiaire ........... 138, 324 Nerode ............................................ 57
Langage machine .......................... 124 Next .............................................. 180
Langage machine absolu ............... 324 Nœud ............................................. 14
Langage machine translatable ...... 324 Nombres binaires impairs ................. 5
Langage source ...................... 130, 289 Nombres binaires pairs ..................... 5
Langages algébriques ...................... 94 Notation conventionnelle .......... 51, 79
Langages formels .............................. 3 Notation graphique ............. 38, 88, 97
Lastpos ......................................... 204 Notation Polonaise ................ 290, 291
LE ................................................ 327 Notation prefixée .......................... 148
Leftmost ....................................... 232 Nullable ................................. 204, 208
Left-parsable ................................. 233
Lex ......................... 142, 167, 196, 335 0
Lexème .......................... 131, 139, 222
0 (log2n) ....................................... 209
Liste chainée .................... 82, 180, 292
0 (n) ..................................... 209, 212
LL ......................................... 94, 279
Opérateur binaire .......................... 290
347

Opérateur unaire ................... 164, 290 Représentation matricielle .............. 35


Opération de branchement ........... 300 Représentation statique ................ 293
Opérations sur les langages .............. 5 Reprise arrière ............................... 139
Optimisation de code ............ 133, 138 Reprise en cas d'erreur. ................. 221
OR ................................. 303 329, 332 Résolution des collisions ................ 212
ORr ............................... 329, 328, 332
s
p
Scanner ......................... 131, 164, 233
Paragrapheurs .............................. 127 KP ................................................ 327
Parcours en post-ordre ................ 292 SKPEQn ....................................... 331
Parcours en préordre .................... 149 SKPEQr ................................ 329, 332
Parcours en profondeur. 148, 233, 321 SKPLT .......................................... 327
Parser ........................................... 223 SKPNE ......................................... 327
Partie analyse ............................... 126 SLR ....................................... 259, 274
Partie arrière ................................ 138 STDS ............................ 112, 306, 320
Partie avant ................................. 138 Steps ..................................... 250, 253
Partie centrale ....................... 130, 290 STM .............................................. 327
Partie finale ................... 130, 289, 324 Structure de contrôle .................... 296
Partie frontale ............... 130, 289, 323 Structure de données adéquate ..... 292
Partie synthèse ...................... 126, 130 Structure de données dynamique .. 209
Pascal.. .............................. 3, 124, 285 Structure de données statique ....... 209
Pile .................................... 13, 21, 115 Structures de données complexes .. 301
Portabilité ............................. 126, 142 SUB ...................................... 327, 334
Préfixe actif.. ................................ 238 Succession de dérivations ........... 8, 10
Pré lecture ............................. 226, 241 Switch ................................... 309, 311
Priorité ............................ 16, 147, 275 Symbole de synchronisation .. 277, 278
Quadruplet .......................... 6, 77, 251 Symbole initial.. .................. 6 1 84, 111
symbole initial de pile ....... 84, 95, 107
R Symbole inutile .............................. 19
RAF ...................................... 100, 119 Symbole marqueur de fin .............. 203
Recherche dichotomique ............... 209 Syntaxe ................................. 112, 127
Reconnaissance .................... 5, 91, 144 Système de numération binaire .... 3, 9
Récursivité à gauche ............ 22, 24, 25 Systèmes de composition ............... 127
Réduction .......................... 12, 46, 280 Systèmes générateurs ....................... 9
Reflet miroir ..................................... 6
Règle de production ............ 7, 79, 153
T
Règles de traduction .............. 197, 199 Table d'analyse ............. 241, 259, 282
Règles sémantiques ........ 144, 152, 155 Table de transition ................ 172, 183
Rehachage ............................. 212, 216 Table des relations d'opérateurs ... 256
Rehachage quadratique ................ 215 Table des symboles ....... 134, 158, 324
Relieur-chargeur ............ 133, 136, 138 Table dichotomique ...................... 211
Répétition conditionnelle .............. 309 Table prédictive .................... 247, 278
Représentation abstraite ............... 292 Tampon ................................ 167, 178
Représentation dynamique ........... 292 Temporaire ........................... 300, 316
Représentation fonctionnelle .......... 35 Théorie des langages ......... 3, 125, 244
Représentation graphique ............... 36 Thompson ...................................... 60
représentation interne ........... 131, 292 Token ............................ 130, 172, 191
348 Index

Top-down ..................................... 223 u


Traduction dirigée par la syntaxe 147 Unité atomique ............. 130, 166, 290
Transducteur à pile ............... 109, 110 Unité lexicale ................ 130, 139, 203
T ransi·t·ion ............................ 34, 42, 72
Transitions étiquetées .................. 101 V
Transitions ordinaires ................... 101
T ransi"t"ions simp
. 1es ................... 39, 41 Variable numérique ....................... 322
Transitions spéciales ..................... 101 Variable temporaire ...... 299, 309, 312
TRUE .................................... 327, 332 Variantes de représentation ........... 77
Type 0 .......................................... 7, 8 Vérification et calcul de types ....... 321
Type 1 .......................................... 7, 8 Version générique .......................... 296
Type 2 ................................. 7, 77, 224 Vocabulaire ...................................... 3
Type 3 .............................................. 7 y
Yacc .............................. 142, 196, 335

Achevé d'imprimer par Corlet, Imprimeur, S.A. - 1411 oCondé-sur-Noireau


N° d'imprimeur : 171791 - Dépôt légal : mai 2015 - Imprimé en France
La collection TECHNOSUP dirigée par Claude Chèze est une sélection d'ouvrages dans toutes les disciplines,
pour les filières technologiques des enseignements supérieurs.
Niveau A Approche (éléments, résumés ou travaux dirigés) IUT - BTS - 1•r cycle
Niveau B Bases (cours avec exercices et problèmes résolus) IUP - Licence
Niveau C Compléments (approfondissement, spécialisation) Écoles d'ingénieurs, Master

L'ouvrage: niveau B (IUP - Licence)


L'ouvrage couvre l'essentiel des connaissances théoriques et pratiques indispensables pour
comprendre ce qu'il y a derrière les compilateurs et les traducteurs.
Les trois premiers chapitres sont entièrement consacrés aux techniques et outils issus de la
théorie des langages, avec successivement :
- un rappel nécessaire pour entrer dans l'ambiance des langages formels,
- une sensibilisation à l'intérêt des automates finis et des expressions régulières,
- une présentation des grammaires à contexte libre et des automates à pile, ainsi que leurs
variantes : les graphes syntaxiques et les réseaux d'automates.
Tous les autres chapitres sont ensuite dédiés aux techniques d'analyse et de traduction avec
successivement :
- une introduction à la compilation,
- l'analyse lexicale, phase initiale de tout système traducteur,
- l'analyse syntaxique qui est la deuxième phase de compilation,
- la phase finale du processus de compilation.
Fruit de plusieurs années d'enseignement des modules de compilation et de théorie des
langages le livre est facile à lire. De nombreux exemples illustratifs sont présentés pour
clarifier et compléter les définitions des nouveaux concepts introduits.
Enfin des exercices d'application sont proposés avec leurs corrigés.

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.

Illustration de couverture : Dessin de Léonard de Vinci.

www.editions-ellipses.fr

You might also like