Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Support Comp Il

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 222

République Algérienne Démocratique et Populaire

Ecole nationale Supérieure en Informatique

Techniques de
Compilation

Ait-Aoudia Samy

PG/0842
Sommaire

CHAPITRE I. Introduction aux Compilateurs ................................................ 1


I.1. Traduction .................................................................................................................... 1
I.2. Structure d’un compilateur ........................................................................................... 2
I.2.2. Analyse lexicale .................................................................................................... 3
I.2.3. Analyse syntaxique ............................................................................................... 3
I.2.4. Analyse sémantique .............................................................................................. 4
I.2.5. Génération de code intermédiaire ......................................................................... 4
I.2.6. Optimisation.......................................................................................................... 5
I.2.7. Génération de code ............................................................................................... 5
I.3. Passes d’un compilateur ............................................................................................... 5
I.3.1. Les compilateurs à deux passes............................................................................. 6
I.3.2. Les compilateurs à une passe ................................................................................ 7
I.3.3. La table des symboles ........................................................................................... 7
I.4. Les Macros ................................................................................................................... 9
I.5. Outils d’écriture de compilateurs ............................................................................... 10
CHAPITRE II. Analyse lexicale ........................................................................ 11
I.1. Introduction ................................................................................................................ 11
II.1. Les expressions régulières ........................................................................................ 11
II.1.1. Chaînes et langages............................................................................................ 11
II.1.2. Définition d’une expression régulière ................................................................ 13
II.2. Automate d'états finis................................................................................................ 14
II.2.1. Formalisme ........................................................................................................ 14
II.2.2. Automate d'états finis déterministe (AFD)......................................................... 14
II.2.3. Automate d'états finis non déterministe (AFN).................................................. 16
II.2.4. Représentation d'un automate ............................................................................ 16
II.3. Transformation d'un automate non déterministe en automate déterministe .............. 17
II.3.1. Algorithme de transformation ............................................................................ 17
II.3.2. Minimisation du nombre d'états de l'AFD ......................................................... 20
II.4. Transformation d'une expression régulière en automate non déterministe................ 23
II.5. contrôle par AFD vs. Contrôle par programme ........................................................ 26
II.6. Utilisation de Lex dans l'analyse lexicale ................................................................. 28
II.6.1. Introduction à LEX ............................................................................................ 28
II.6.2. Expressions régulières de LEX .......................................................................... 29
II.6.3. Structure d'un programme LEX ......................................................................... 30
II.6.4. Quelques fonctions et variables de LEX ............................................................ 33
II.6.5. Exemple de programme Lex .............................................................................. 34

i
II.6.6. Utilisation des états d’analyse : exemple ........................................................... 36
II.7. Exercices................................................................................................................... 38
CHAPITRE III. Principes de l'Analyse Syntaxique ......................................... 41
III.1. Introduction ............................................................................................................. 41
III.2. Les grammaires non-contextuelles .......................................................................... 41
III.3. Dérivations et langages............................................................................................ 42
III.3.1. Dérivations ....................................................................................................... 42
III.3.2. Langages .......................................................................................................... 44
III.3.3. Arbre de dérivation .......................................................................................... 44
III.3.4. Ambiguïté ......................................................................................................... 45
III.4. Transformations de Grammaires non-contextuelles ................................................ 46
III.4.1. Propriétés particulières d'une grammaire ......................................................... 46
III.4.2. Elimination des symboles inutiles .................................................................... 46
III.4.3. Elimination des productions unitaires .............................................................. 48
III.4.4. Rendre une grammaire ε-libre .......................................................................... 48
III.4.5. Substitution des non-terminaux ........................................................................ 49
III.5. Principe des analyses descendantes et ascendantes ................................................. 50
III.6. Backus–Naur Form (BNF) ...................................................................................... 51
III.7. Exercices ................................................................................................................. 52
CHAPITRE IV. Analyse syntaxique descendante............................................. 55
IV.1. Introduction ............................................................................................................. 55
IV.2. Analyse syntaxique LL ............................................................................................ 55
IV.2.1. Analyse syntaxique LL(1) ................................................................................ 56
IV.2.2. Analyse syntaxique LL(k) ................................................................................ 65
IV.3. Analyse syntaxique par descente récursive ............................................................. 67
IV.3.1. Conditions préalables à une descente récursive ............................................... 67
IV.3.2. Ecriture des procédures .................................................................................... 67
IV.3.3. Exemple d'analyse ............................................................................................ 68
IV.4. Exercices ................................................................................................................. 71
CHAPITRE V. Analyses syntaxiques ascendantes .......................................... 75
V.1. Analyse par précédence d'opérateurs ........................................................................ 75
V.1.1. Grammaire d'opérateurs..................................................................................... 75
V.1.2. Relations de précédence d'opérateurs ................................................................ 75
V.1.3. Grammaire de précédence d'opérateurs ............................................................. 76
V.1.4. Algorithme d'analyse de précédence d'opérateurs ............................................. 77
V.1.5. Exemple d'analyse ............................................................................................. 78
V.2. Analyse par précédence simple ................................................................................ 78
V.2.1. Relations de précédence simple ......................................................................... 79
V.2.2. Grammaire de précédence simple ...................................................................... 79
V.2.3. Algorithme d'analyse de précédence simple ...................................................... 79

ii
V.2.4. Exemple d'analyse ............................................................................................. 80
V.2.5. Optimisation ...................................................................................................... 82
V.2.6. Transformations de grammaires simples ........................................................... 84
V.3. Analyse par précédence faible .................................................................................. 85
V.3.1. Grammaires de précédence faible...................................................................... 85
V.3.2. Analyse par précédence faible ........................................................................... 85
V.3.3. Exemple d'analyse ............................................................................................. 86
V.3.4. Optimisation ...................................................................................................... 87
V.4. Analyse par la méthode LR(k) .................................................................................. 88
V.4.1. Analyse Contextuelle ......................................................................................... 88
V.4.2. Méthode pratique pour la construction d'analyseurs LR .................................... 98
V.4.3. Utilisation des grammaires ambiguës .............................................................. 114
V.4.4. Gestion des erreurs en analyse LR................................................................... 116
V.4.5. Classification des grammaires ......................................................................... 117
V.5. Exercices ................................................................................................................ 119
CHAPITRE VI. Traduction Dirigée par la Syntaxe ....................................... 125
VI.1. Langages intermédiaires ........................................................................................ 125
VI.1.1. Notation post-fixée ......................................................................................... 125
VI.1.2. Qudruplets ...................................................................................................... 126
VI.1.3. Triplets et Triplets indirects ........................................................................... 127
VI.1.4. Arbres abstraits .............................................................................................. 128
VI.2. Définitions dirigées par la syntaxe ........................................................................ 128
VI.2.1. Attributs des symboles de la grammaire......................................................... 128
VI.2.2. Définitions n'utilisant que des attributs synthétisés ........................................ 129
VI.2.3. Définitions utilisant des attributs hérités ........................................................ 130
VI.3. Schémas de traduction........................................................................................... 131
VI.3.1. Définition ....................................................................................................... 131
VI.3.2. Conception d'un schéma de traduction ........................................................... 131
VI.3.3. Méthodologie de génération de code intermédiaire ....................................... 132
VI.4. Traduction descendante......................................................................................... 132
VI.4.1. Emplacement des routines sémantiques ......................................................... 132
VI.4.2. Cas général ..................................................................................................... 133
VI.4.3. Conception d'un traducteur descendant .......................................................... 134
VI.5. Traduction ascendante........................................................................................... 142
VI.5.1. Emplacement des routines sémantiques ......................................................... 142
VI.5.2. Définitions L-attribuées.................................................................................. 142
VI.5.3. Elimination des actions intérieures................................................................. 142
VI.5.4. Schéma de traduction générant des quadruplets ............................................. 143
VI.6. YACC ................................................................................................................... 149
VI.6.1. Grammaires YACC ........................................................................................ 149
VI.6.2. Structure d'un programme YACC .................................................................. 149

iii
VI.6.3. Exemple de programme YACC...................................................................... 151
VI.6.4. Variables et commandes de YACC ................................................................ 152
VI.6.5. Fonctionnement de l'analyseur ....................................................................... 152
VI.7. Exercices ............................................................................................................... 158
CHAPITRE VII. Environnements d'exécution ............................................... 163
VII.1. Introduction ......................................................................................................... 163
VII.2. Procédures et activations ..................................................................................... 163
VII.2.1. Procédures .................................................................................................... 163
VII.2.2. Arbre d'activation.......................................................................................... 165
VII.2.3. Pile de contrôle ............................................................................................. 166
VII.3. Organisation de l'espace mémoire........................................................................ 167
VII.3.1. Répartition de la mémoire à l'exécution ........................................................ 167
VII.3.2. Bloc d'activation ........................................................................................... 168
VII.4. Allocation de la mémoire ..................................................................................... 169
VII.4.1. Allocation statique ........................................................................................ 169
VII.4.2. Allocation en pile .......................................................................................... 170
VII.4.3. Allocation dans le tas .................................................................................... 173
VII.5. Accès aux noms non locaux ................................................................................. 174
VII.5.1. Blocs ............................................................................................................. 174
VII.5.2. Portée statique sans déclaration de procédures imbriquées .......................... 175
VII.5.3. Portée statique avec déclaration de procédures imbriquées .......................... 176
VII.5.4. Portée dynamique ......................................................................................... 177
VII.6. Passage de paramètres ......................................................................................... 177
VII.6.1. Passage par valeur......................................................................................... 178
VII.6.2. Passage par référence .................................................................................... 178
VII.6.3. Passage par copie-restauration ...................................................................... 178
VII.6.4. Passage par nom ........................................................................................... 179
VII.7. Exercices .............................................................................................................. 180
CHAPITRE VIII. Production de Code.......................................................... 183
VIII.1. Introduction ........................................................................................................ 183
VIII.2. Machine Cible .................................................................................................... 183
VIII.3. Blocs de base et graphes de flot de contrôle ....................................................... 185
VIII.3.1. Blocs de Base .............................................................................................. 185
VIII.3.2. Transformations sur les blocs de Base......................................................... 187
VIII.3.3. Construction du graphe de flot de contrôle.................................................. 188
VIII.4. Un générateur de Code simple............................................................................ 190
VIII.4.1. Informations d'utilisation ultérieure ............................................................. 190
VIII.4.2. Descripteurs de registres et d'adresses ......................................................... 191
VIII.4.3. Algorithme de production de code .............................................................. 192
VIII.4.4. Production de code pour d'autres types d'instructions ................................. 194

iv
VIII.4.5. Production de code à partir de DAG ........................................................... 195
VIII.4.6. Assignation globale de registres .................................................................. 205
VIII.5. Exercices ............................................................................................................ 209
BIBLIOGRAPHIE ............................................................................................. 212
ANNEXE A. Classification des Grammaires ................................................... 213
ANNEXE B. Formes Normales des Grammaires ............................................ 214

v
I. Introduction aux Compilateurs

CHAPITRE I. INTRODUCTION AUX


COMPILATEURS

I.1. TRADUCTION
Les programmes qui convertissent un programme de l'utilisateur écrit en
un langage quelconque en un programme écrit dans un autre langage sont
appelés des traducteurs.
Le langage dans lequel on écrit le programme originel est la langage
source, tandis que le langage après conversion est le langage cible (ou
langage objet).
Si le langage source est un langage de haut niveau (par exemple C ou
Pascal) et le langage objet est de bas niveau (langage machine) alors un tel
traducteur est appelé compilateur (figure I.1).

programme programme
→ COMPILATEUR →
source objet

Figure I.1. Compilateur

Exécuter un programme écrit dans un langage de haut niveau est un


processus à deux étapes : le programme source est compilé donnant un
programme objet ; ce programme est alors chargé en mémoire centrale et
exécuté.

Autres traducteurs
 Lorsque le langage source est essentiellement une représentation
symbolique du langage d'une machine, le traducteur est appelé un
assembleur.
 Le pré-processeur désigne un traducteur qui converti un programme
écrit dans un langage de haut niveau en un programme équivalent dans
un autre langage de haut niveau.

1
I. Introduction aux Compilateurs
 Un interprète (ou interpréteur) est un traducteur qui ne génère pas un
exécutable comme un compilateur mais qui exécute les instructions du
programme source l'une après l'autre avec les données correspondantes.
Les langages LISP (LISt Processing) ou BASIC sont des langages
interprétés.

Remarque
 Dans ce cours on utilisera le terme "Compilateur" au sens large i.e. les
notions étudiées peuvent être appliquées à d'autres traducteurs.

I.2. STRUCTURE D’UN COMPILATEUR


Le processus de compilation est constitué de plusieurs phases comme le
montre la figure 1.2 suivante :

programme source

Analyse
lexicale

Analyse
syntaxique

Analyse
sémantique
Table des gestion

symboles des erreurs
code
intermédiaire

optimisation
de code

génération
de code


programme objet
Figure I.2. Structure d’un compilateur.

2
I. Introduction aux Compilateurs

I.2.2. Analyse lexicale

C’est au cours de cette phase que le flot de caractères formant le


programme source est lu de gauche à droite et groupé en unités lexicales, qui
sont des suites de caractères ayant une signification collective. Les
identificateurs, les mots-clés, les constantes et opérateurs sont des unités
lexicales.
Les commentaires et les blancs séparant les caractères formant les unités
lexicales sont éliminés au cours de cette phase.
Exemple :
• Soit l’instruction Pascal suivante :
IF (5 = MAX) THEN GOTO L1 ;

Les unités lexicales identifiées sont :


IF ( 5 = MAX ) THEN GOTO L1 .

L’analyseur lexical associe à chaque unité lexicale un code qui spécifie


son type et une valeur (un pointeur vers la table des symboles).
Après analyse de l’instruction précédente on peut avoir la sortie suivante
:
if ( [CONST, 341] = [ID, 729] ) then goto [LABEL, 554]

TABLE DES SYMBOLES

341 Constante ; Integer ; Valeur = 5

554 Label ; valeur = L1

729 Variable ; Integer ; Valeur = MAX


.
.

I.2.3. Analyse syntaxique

Cette phase consiste à regrouper les unités lexicales du programme


source en structures grammaticales (i.e. vérifie si un programme est
correctement écrit selon la grammaire qui spécifie la structure syntaxique du
langage). En général, cette analyse est représentée par un arbre.

3
I. Introduction aux Compilateurs

Exemple :
• Considérons la grammaire suivante des expressions arithmétiques :
E→E+T|T
T→T*F|F
F → (E) | id
et soit à analyser la phrase suivante : A + B * C.

Analyse
A+B*C → → id1 + id2 * id3
lexicale

Après l’analyse syntaxique on obtient l’arbre suivant (figure 1.3):


E
E + T

T T * F

F F id

id id
Figure I.3. Arbre syntaxique

I.2.4. Analyse sémantique


Cette phase vérifie que les opérandes de chaque opérateur sont conformes
aux spécifications du langage source. Par exemple, beaucoup de définitions
de langages de programmation exigent que le compilateur signale une erreur
chaque fois qu’un nombre réel est employé pour indicer un tableau.

I.2.5. Génération de code intermédiaire


A l’issue de l’analyse syntaxique et de l’analyse sémantique, certains
compilateurs construisent explicitement une représentation intermédiaire du
programme source. Cette représentation doit avoir deux propriétés
importantes : elle doit être facile à produire et facile à traduire en langage
cible.
Plusieurs formes intermédiaires sont possibles. La forme intermédiaire
‘appelée code à trois adresses’ est largement utilisée. Dans cette forme,
chaque instruction a au plus trois opérandes.

4
I. Introduction aux Compilateurs
L’expression arithmétique (id1 + id2 * id3) peut être traduite en code à
trois adresses de la façon suivante (temp1 et temp2 sont des temporaires):
temp1 := id2 * id3
temp2 := id1 + temp1

I.2.6. Optimisation
Cette phase tente d’améliorer le code intermédiaire pour réduire le temps
d’exécution ou l’occupation mémoire. Quelques unes des opérations
d’optimisation sont :
• Déplacement de code invariant :
le but est d’extraire du corps de la boucle les parties du code qui sont des
invariants (nécessitent une seule exécution). Le nombre total d’instructions
exécutées est diminué.
• Elimination du code inutilisé :
le compilateur sait reconnaître les parties du code qui ne sont pas utilisées (la
raison peut être une erreur de programmation).
• Elimination des sous-expressions communes :
dans le cas où la valeur d’une expression est recalculée en plusieurs endroits
du programme, l’optimiseur gère le résultat de cette expression et le réutilise
plutôt que de refaire le calcul.

I.2.7. Génération de code


Le but de cette phase est de convertir le code intermédiaire optimisé en
une séquence d’instructions machine.
Exemple :
l’instruction du code intermédiaire T:=A+B est convertie en instructions
machine suivantes :
LOAD A
ADD B
STORE T

I.3. PASSES D’UN COMPILATEUR


Lors de l’implémentation d’un compilateur (ou un traducteur en général),
des parties d’une ou plusieurs phases sont combinées pour former un
module appelé passe.
Une passe lit le programme source ou la sortie de la passe précédente,
effectue les transformations spécifiées par ses phases et produit un fichier
intermédiaire qui sera lu par la passe suivante (ou le programme objet s’il
s’agit de la dernière passe).
5
I. Introduction aux Compilateurs
Considérons une instruction de saut à une étiquette L. L'assembleur ne
peut assembler (traduire) cette instruction, puisqu'il ne connaît pas l'adresse
de L ; cette adresse peut être à la fin du programme et il est impossible à
l'assembleur de la trouver avant d'avoir pratiquement lu tout le programme.
Ce problème provient de la référence en avant de L : L a été utilisée avant
d'être définie. Les références en avant peuvent être gérées de deux façons.

I.3.1. Les compilateurs à deux passes


Le compilateur lit le programme deux fois ; dans la première passe, on
enregistre dans une table la définition des symboles (y compris les
étiquettes).
Pour affecter une valeur à un symbole du champs étiquette d'une
instruction, le compilateur doit connaître l'adresse à laquelle sera placée
cette instruction à l'exécution du programme. Il faut donc garder trace de
l'adresse de l'instruction en cours de la traduction. Pour cela, le compilateur
gère une variable que l'on appelle compteur d'emplacement mis à zéro au
début de la première passe et incrémentée de la longueur de l'instruction à
chaque instruction traitée (tableau I.1).
Etiquette Instruction Largeur compteur
(octets) d’emplacement
Label1 : X := I 5 0
Y := J 5 5
Label2 : Z := K 5 10
X := I * I 6 15
Y := J * J 6 21
TOTO: Z := K * K 6 27

Tableau I.1. Gestion du compteur d’emplacement.

Dans la table des symboles, les étiquettes auront les valeurs suivantes
(tableau I.2) :
Identificateur Valeur Autres informations
Label 1 0
Label 2 10
TOTO 27
. .
. .
. .

Tableau I.2. Table des symboles.

6
I. Introduction aux Compilateurs
Lorsque la deuxième passe commence, les valeurs de tous les symboles
sont connues de telle sorte qu'il ne subsiste plus de référence en avant et que
chaque instruction puisse être traduite.

I.3.2. Les compilateurs à une passe

Dans cette seconde méthode, la traduction est faite en une seule passe :
lorsqu'on rencontre une instruction qui contient une référence en avant, on
ne génère pas de sortie, mais on met à jour une table indiquant que cette
instruction n'a pas été traduite. A la fin de la lecture du programme source et
donc lorsque tous les symboles ont été définis, on peut traduire ces
instructions.
Le problème est que si le programme contient beaucoup de référence en
avant, la table des instructions en attente de traduction peut devenir énorme,
au point de ne plus tenir en mémoire. Un compilateur à une passe est
également plus complexe qu'un compilateur à deux passes.

I.3.3. La table des symboles

Pendant l'analyse du programme source le compilateur collecte et utilise


les informations concernant les noms qui y apparaissent. Ces informations
sont stockées dans une structure de données appelée Tables des Symboles.
 Les informations concernant un nom apparaissant dans un programmes
source sont typiquement :
 Chaîne de caractères composant le nom
 Son type (entier, réel, …)
 Sa forme (variable simple, une structure, ...)
 Son adresse mémoire
 D’autres paramètres selon le langage considéré.
 Chaque entrée dans la table des symboles est référencée par le
couple :
(nom, informations)
 Les opérations typiques sur la table des symboles sont :
 Déterminer si un nom est dans la table des symboles.
 Ajouter un nom est dans la table des symboles.
 Accéder aux informations relatives à un nom.
 Ajouter de nouvelles informations relatives à un nom.
 Entrer dans une portée.
 Sortir d’une portée.
7
I. Introduction aux Compilateurs

Remarques :
i. Les informations contenues dans la table des symboles sont collectées
durant les étapes d’analyse lexicale et syntaxique. Durant l’analyse
lexicale, chaque fois qu’un nom est rencontré, on le recherche dans la
table des symboles. S’il n y est pas déjà alors il est inséré. C’est durant
l’analyse syntaxique que les informations sont insérées.
ii. Ces informations sont utilisées durant l’analyse sémantique, la génération
de code, l’optimisation du code et la détection des erreurs.
iii. Le programme objet considère les objets liés aux noms et ignore les noms
qui les ont introduits. A la traduction chaque nom est remplacé par l’objet
à l’exécution qu’il désigne.
Il existe plusieurs méthodes pour structurer et gérer cette table des
symboles qui, toutes, tentent de simuler une mémoire associative, c'est à dire
un ensemble de doublés (symbole, valeur). L'objectif est de pouvoir accéder
rapidement à la table dans le but de rechercher ou insérer une information.
Lorsqu'on recherche un symbole, une procédure parcourt la table jusqu'à
trouver le symbole correspondant. Cette méthode est simple à programmer,
mais lente. Une autre méthode est de gérer la table des symboles par ordre
alphabétique et d'utiliser un algorithme de recherche dichotomique. Cette
méthode est plus rapide que la recherche linéaire.
Une autre façon de procéder est l'adressage dispersé (hash coding). Cette
méthode implique d'avoir une fonction de dispersion qui "mappe" les
symboles sur un intervalle d'entiers de 0 à k-1.
La fonction de dispersion peut être, par exemple, réalisée en multipliant
entre eux les codes ASCII des caractères composant le symbole, en ignorant
les dépassements de capacité éventuels et en prenant le résultat modulo k
(on peut imaginer n'importe qu'elle autre fonction, pourvu que la dispersion
obtenue soit à peu près uniforme).
Les symboles sont alors stockés dans une table comportant k
emplacements numérotés de 0 à k-1. Il se peut que lorsqu'on applique la
fonction de dispersion à plusieurs symboles, on obtienne la même valeur i.
On parle alors de problème de collision. Les symboles ayant une même clé
peuvent être stockés dans une liste chaînée à laquelle on accédera à partir de
l'entrée i de la table. Avec n symboles et k entrées, la longueur moyenne de
la liste sera n/k. Cette technique de hash coding est illustrée à la figure 1.4 (k
= 5).

8
I. Introduction aux Compilateurs

I.4. LES MACROS

Les programmeurs ont souvent besoin de répéter plusieurs fois dans un


programme la même suite d'instructions. La façon la plus simple est de
réécrire les instructions chaque fois qu'on en a besoin mais, si cette suite
d'instructions est longue, cela devient fastidieux.
Une autre façon de faire consiste à transformer cette suite d'instructions
en une procédure qu'on appellera si nécessaire. Mais cela présente
l'inconvénient d'exiger l'exécution d'une instruction d'appel de procédure et
d'une instruction de retour après chaque appel. Si on utilise fréquemment la
suite d'instructions, l'appel de procédure peut ralentir énormément
l'exécution du programme. Le mécanisme de création de macro-instruction
est une solution plus efficace.
Pour donner un nom à un morceau de texte de programme, on utilise une
définition de macro. Lorsqu'une macro a été définie, le programmeur utilise
son nom à la place de tout le texte. On peut considérer une macro-
instruction comme une abréviation de texte.
En général, pour la définition des macros, on trouve toujours :
1 - un en-tête de macro qui lui affecte un nom
2 - le texte du corps de la macro
3 - une pseudo-instruction marquant la fin de la définition.

La figure 1.5.a montre un programme qui échange deux fois le contenu


des variables A et B. On peut remplacer ces instructions par une macro,
comme le montre la figure 1.5.b.
• • MACRO ECHANGE
• • TMP1 := B
• • B := A
• • A := TMP1
• • END-MACRO
TMP1 := B •
B := A ECHANGE
A := TMP1 •
• •
• •
• •
TMP1 := B •
B := A ECHANGE
A := TMP1 •

(a) (b)

Figure I.4. Suites d’instructions : (a) sans macro (b) avec macro

9
I. Introduction aux Compilateurs
Lorsque le compilateur rencontre une définition de macro, il l'enregistre
dans une table et chaque fois que le nom de la macro apparaît, il le remplace
par le corps de la macro. l'opération de remplacement est appelée expansion
de macro.
L'expansion se fait pendant la phase de traduction et non pendant la phase
d'exécution du programme. Les programmes des figures 1.5.a et 1.5..b
produisent le même code en langage machine.

I.5. OUTILS D’ECRITURE DE COMPILATEURS


Des outils spécialisés ont été mis au point pour faciliter l’implantation de
certaines phases d’un compilateur. Les outils qui rencontrent le plus grand
succès sont ceux qui cachent les détails de l’algorithme de construction.
Parmi ces outils on trouve :
• générateurs d’analyseurs lexicaux;
• générateurs d’analyseurs syntaxiques;
• générateurs de compilateurs (compiler-compiler, compiler-generator,…)
Exemple :
Sous Unix, on dispose de deux outils bien connus que sont LEX et
YACC.
LEX est un générateur d’analyseurs lexicaux.
YACC (Yet Another Compiler-Compiler) est un générateur de
compilateurs

10
2. Analyse Lexicale

CHAPITRE II. ANALYSE LEXICALE

I.1. INTRODUCTION

Le but de l'analyse lexicale est de transformer une suite de symboles


en entités (une entité peut être par exemple un nombre, un signe "+", un
identificateur, etc.). Une fois cette transformation effectuée, la main est
repassée à l'analyseur syntaxique. Le but de l'analyseur lexical est donc de
"consommer" et "transformer" des symboles du texte source (programme
écrit en langage de haut niveau) et de les fournir à l'analyseur syntaxique.

Les expressions régulières permettent de décrire d'une manière


concise et compacte les entités lexicales. Les expressions régulières sont par
exemple utilisées par les générateurs d'analyseurs lexicaux comme l'outil
Lex. Pour effectuer l'analyse lexicale ("informatiquement" parlant), les
expressions régulières doivent être "transformées en automates d'états finis
dont l'implémentation est simple et l'exécution relative de reconnaissance
des entités lexicales est rapide.

II.1. LES EXPRESSIONS REGULIERES

II.1.1. Chaînes et langages

II.1.1.1. Chaîne
Définitions
a. Un alphabet (dénoté généralement par le symbole Σ) est un ensemble
fini de symboles.
b. Une chaîne est séquence finie de symboles (le terme mot est également
utilisé pour désigner une chaîne)
c. La longueur d’une chaîne ω est notée |ω| est désigne le nombre de
symbole de la chaîne ω.
d. La chaîne vide est notée ε et est de longueur 0.

11
2. Analyse Lexicale

Exemples :
 Σ={0,1} est un alphabet
 100010 est une chaîne de longueur 6
 |100010|=6

II.1.1.2. Concaténation
Définition
Soient ϖ et ψ deux chaînes. La concaténation de ϖ et de ψ notée ϖ.ψ
ou ϖψ est la chaîne formée des symboles de ϖ suivis des symboles de ψ.
Exemples :
 abc.de = abcde
 ε.ϖ = ϖ.ε = ϖ
 ϖ1 = ϖ et ϖ2 = ϖ.ϖ
 ϖi : concaténation i fois de la chaîne ϖ
 ϖ0 = ε

II.1.1.3. Langage
Définitions
a. On utilise le terme langage pour désigner un ensemble de chaînes
formées à partir d’un alphabet spécifique.
b. La concaténation de deux langages L et M notée L.M ou LM est définie
comme suit : L.M = {ϖψ | ϖ ∈ L et ψ ∈ M}.
c. L’union de 2 langages L et M est définie par L∪M = {ϖ| ϖ∈L ou
ϖ∈M}
d. Les opération de fermeture et de fermeture positive d’un langage L sont
notées L* et L+ est sont définies comme suit :

L* = ∪ Li
i =0

L = L.Li
+

Exemples :
 L={0,01,110} et M={10,110}
alors L.M = {010,0110,01110,11010,110110}
 Li : concaténation i fois de L
 L0 = {ε}

12
2. Analyse Lexicale

 L.{ε} = {ε}.L = L

II.1.2. Définition d’une expression régulière


On appelle expression régulière sur un alphabet ∑ les expressions qui
sont construites à partir des règles suivantes :
• Une lettre de l'alphabet a désigne le langage {a}.
• Epsilon: ε désigne le langage {ε}.
• Si M et N sont deux expressions régulières décrivant les langages LM
et LN alors :
 (M) · (N) (Concaténation) désigne le langage LM . LN
 M | N (Alternative) désigne le langage LM ∪ LN
 M* (Répétition) désigne le langage LM*

Exemples :

 a*= ∪ {ai } i.e. l’ensemble des chaînes de zéro ou plusieurs a.
i =0

 L'expression régulière a+ (= a.a* ) désigne l'ensemble des chaînes de 0


ou plusieurs a.

 (a|b)*= ∪ {a,b}i
i =0

 L'expression régulière suivante décrit les identificateurs définis comme


des chaînes commençant par une lettre suivies de lettres et de chiffres :
identificateur = {lettre}.({lettre} | {chiffre})*
Quelques règles
 R, S et T sont des expressions régulières :

√ R|S=S|R (la loi | est commutative)


√ R | (S | T) = (R | S) | T (la loi | est associative)
√ R.(S.T) = (R .S).T (la loi . est associative)
√ R.(S | T) = R.S [ R.T (distributivité de . sur |)
√ ε.R = R.ε (ε élément neutre)
√ a.b* = a.(b*) (* a la priorité sur .)
√ a | b.c = a. | (b.c) (. a la priorité sur |)
13
2. Analyse Lexicale

Remarque
 L'opérateur de concaténation . est souvent omis dans l'écriture des
expressions régulières. Le fait de faire suivre deux expressions sans
séparateur signifie leur concaténation.

II.2. AUTOMATE D'ETATS FINIS

II.2.1. Formalisme
Définition :
• Un automate fini M est un quintuple (Q, Σ, δ, q0, F) où :
 ∑ est un alphabet;
 Q est un ensemble fini d'états;
 δ: Q × Σ → Q est la "fonction" de transition;
 q0 est l'état initial;
 F est un ensemble d'états finaux.

Propriété :
• Le langage L(M) reconnu par l'automate M est l'ensemble { w | δ (q0, w)
∈ F} des mots permettant d'atteindre un état final à partir de l'état initial
de l'automate.

II.2.2. Automate d'états finis déterministe (AFD)


Définition :
 Un automate d'états finis est déterministe si :
1. Il n'existe pas de ε-transition
2. Pour chaque état e et un symbole d'entrée a, alors il existe au plus un
arc étiqueté a sortant de l'état e.
Exemple :
 Automate déterministe correspondant à l'expression régulière aab | bbb :

14
2. Analyse Lexicale

Figure II.1. Automate déterministe de aab | bbb.


• Σ = {a,b}
• Q = {q0, q1, q2, q3, F}
• δ = {(q0, a) |→ q1, (q0, b) |→ q2, (q1, a) |→ q3, (q2, b) |→ q3, (q3, b)
|→ F}
• État initial q0 et un seul état final F.
Remarque :
 Il est facile de simuler un automate d'états finis déterministe par un
programme simple.
Simulation d'un automate d'états finis déterministe :
 On désire analyser une chaîne d'entrée S terminée par un caractère
spécial '#" à l'aide d'un automate d'états finis déterministe avec un état de
départ et un ensemble d'états d'acceptation F.
 On dispose des fonctions suivantes :
√ Transiter (e, c) : donne l'état de l'automate vers lequel il existe une
transition depuis l'état e sur le caractère d'entrée c.
√ CarSuiv () : retourne le prochain caractère à analyser de la
chaîne S.
 Algorithme de reconnaissance :
e:= e0 ;
c: = CarSuiv ;
Tant que (c ≠ '# ' et e ≠ ∅)
Faire e:= Transiter (e, c);
c:= CarSuiv();
Fait;
Si e ∈ F
Alors "Chaîne acceptée"
Sinon " Chaîne refusée"
Fsi

15
2. Analyse Lexicale

II.2.3. Automate d'états finis non déterministe (AFN)


Définition :
 Un automate d'états finis est non déterministe si :
√ Il peut exister des ε-transitions
√ Un symbole d'entrée a peut étiqueter plusieurs arcs sortants d'un
même état e.
Exemple :
 Automate déterministe correspondant à l'expression régulière aab*a |
bb*b :

Figure II.2. Automate déterministe de aab*a | bb*b.

Remarques :
 Les automates d'états finis non déterministes sont plus facile à obtenir
que les automates déterministes.
 Mais il est "difficile" de simuler un automate d'états finis non
déterministe par un programme simple.

II.2.4. Représentation d'un automate


• La fonction δ de domaine fini Q × Σ peut être représentée par une matrice
de dimension 2 dont les éléments sont les états (pour un automate
déterministe) ou ensembles d'états (pour un automate non-déterministe)
définissant δ.
• On peut choisir une représentation pleine (tableau de tableaux) ou creuse
(tableau de listes) selon la situation. La première est bien sûr plus efficace
en temps mais plus gourmande en espace.
16
2. Analyse Lexicale

• Dans le cas d'une matrice creuse, on peut aussi économiser de l'espace en


superposant les tableaux de domaines disjoints (astuce souvent utilisée en
pratique qui a l'efficacité en temps de la représentation pleine et souvent
l'efficacité en espace de la représentation creuse).
• Représentation de l'automate de la section 3.3 par une table :

a b
{q0} {q1} {q2}
{q1} {q3} -
{q2} - {q2, q4}
{q3} {q4} {q3}
{q4} - -

Tableau II.1. Table de transition.

II.3. TRANSFORMATION D'UN AUTOMATE NON


DETERMINISTE EN AUTOMATE DETERMINISTE

II.3.1. Algorithme de transformation

 On dispose d'un automate d'états finis non déterministe AFN dont :


√ e0 : état initial de l'AFN ;
√ T : un ensemble d'états de l'AFN ;
√ e : un état de l'AFN ;
 On dispose des fonctions suivantes :
√ ε-fermeture(e) : ensemble des états de l'AFN accessibles depuis
l'état e de l'AFN par ε-transitions (état e inclus).
√ ε-fermeture(T) : ensemble des états de l'AFN accessibles depuis
un état e appartenant à T par des ε-transitions (l'ensemble T inclu).
√ Transiter (T, a): ensemble des états de l'AFN vers lesquels il
existe une transition dans l'AFN sur le symbole a à partir d'un état
e appartenant à T.

17
2. Analyse Lexicale

 Algorithme :
D-états : ensemble d'états de l'AFD à construire ;
D-Trans : la table de transition de l'AFD ;
Initialement ε-fermeture(e0) unique état de D-états et il est
non marqué ;
Tant que ∃ un état non marqué T dans D-états
Faire marquer T ;
Pour chaque symbole d'entrée a
Faire U:= ε-fermeture (Transiter (T, a));
Si U ∉ D-états
Alors ajouter U à D-états et U est non marqué
FinSi;
D-Trans [T, a] := U;
Fait;
Fait;
L'état initial de l'AFD est ε-fermeture(e0);
Un état f est un état final de l'AFD si ∃ g ∈ f et g état final
de l'AFN.

 Fonction ε-fermeture (T) :


Empiler les états de T dans une pile ;
Initialiser ε-fermeture (T) à T ;
Initialement ε-fermeture(e0) unique état de D-états et il est
non marqué ;
Tant que pile non vide
Faire dépiler-dans (t) ;
Pour chaque état u avec une ε-transition de t à u
Faire
Si u ∉ ε-fermeture (T)
Alors ajouter u à ε-fermeture (T) ; empiler (u);
FinSi;
Fait;
Fait;

18
2. Analyse Lexicale

Exemple :
 Transformation de l'automate d'états finis non déterministe suivant
correspondant à l'expression régulière a*b | b*a en un automate
déterministe :
a
b
1 3
ε

0
ε
2 4
a
b
Figure II.3. Automate non déterministe de a*b | b*a.
 Construction de l'AFD :
√ ε-fermeture (0) = {0, 1, 3}
√ Table de transition de l'AFD donnée ci après :
a b
{0, 1, 3} {1, 4} {2, 3}
{1, 4} {1} {2}
{2, 3} {4} {3}
{1} {1} {2}
{2} - -
{4} - -
{3} {4} {3}
√ Etat initial : ε-fermeture (0) = {0, 1, 3}
√ Etats finaux : {1, 4}, {2, 3}, {2}, {4}
√ Table de transition de l'AFD avec états renommés :

a b
S T U
T V X
U Y Z
V V X
X - -
Y - -
Z Y Z
19
2. Analyse Lexicale

√ Le schéma de l’automate d’états finis déterministe est donné par la


figure ci-après :
a
a V
T
b
a b X

a Y
b
U a
b Z
b

Figure II.4. Automate déterministe de a*b | b*a.

II.3.2. Minimisation du nombre d'états de l'AFD


 L'automate d'états finis déterministe AFD pour effectuer l'analyse
lexicale est généralement obtenu par un processus automatique du type :
√ Conversion Expression régulière → automate d'états finis non
déterministe AFN
√ Puis Conversion AFN → automate d'états finis déterministe AFD

Remarque :
 L'automate d'états finis déterministe AFD obtenu ainsi n'est pas
forcément minimal en nombre d'états. Il faudrait donc minimiser le
nombre d'états de l'AFD par algorithme pour que la table de transition
sur machine soit la plus petite possible.

20
2. Analyse Lexicale

 Algorithme de "minimisation" :
i) Construire une partition initiale ∏ des états de l'AFN avec 2
groupes ; les états d'acceptation et les autres états ;
ii) Obtenir une nouvelle partition ∏' par :
 Pour chaque groupe G de ∏

 Faire

 Partition de G en sous-groupes de telle sorte que deux états s et t


soient dans le même groupe si et seulement si, pour tout symbole
a,les états s et t ont des transitions sur a vers des états du même
groupe de ∏ ;
 Remplacer G dans ∏' par tous les sous-groupes ainsi formés
 FinPour;

iii) Si ∏' = ∏ Alors aller à iv) Sinon ∏ = ∏' ; aller à ii) FinSi ;
iv) L'état de départ de l'AFD est le représentant du groupe qui
contient l'état de départ de l'AFN ; les états d'acceptation de l'AFD
sont les représentants des états finaux de l'AFN;
v) Supprimer de l'AFD les états non accessibles.
Application 1:
Considérer l’automate d’états finis déterministe suivant :
a
a
S T

a a
b
b

U V
b b
La table de transition de l’AFD précédent est la suivante où S est l’état
initial et les états T et V sont finaux.

a b
S T U
T T V
U T U
V T V

21
2. Analyse Lexicale

Après application de l’algorithme précédent on obtient l’AFD minimal


suivant :
b a|b

S,U T,V

Remarque :
Il est à noter que dans cet exemple la première partition entre états finaux et
états non finaux a suffi pour obtenir l’AFD minimal.

Application 2:
Considérer la table de transition suivante d’un automate d’états finis
déterministe. Les états sont numérotés de 0 à 5. L’état 0 est l’état initial. Les
états 1, 3 et 5 sont les états finaux. Les transitions se font sur les caractères c
et p. La table est présentée de manière à faciliter la compréhension de
l’algorithme de minimisation.

c p
0 1 4
2 3 -
4 5 -
1 1 2
3 3 -
5 5 -

Après application de l’algorithme de minimisation, on obtient 4 états


désignés par les lettres A, B, C et D qui désignent respectivement les
ensembles d’états {0}, {2,4}, {1} et {3,5}.

22
2. Analyse Lexicale

Remarques :
c
c
A C

a
p p

c
B D
c
 L’utilisation d’un AFD ou d’un AFD minimal pour analyser un mot d’un
langage donné induit le même nombre de transitions.
 L’utilisation de l’AFD minimal implique un gain en espace mémoire et
éventuellement en temps d’exécution si la table de transition non
"minimisée" entraîne des défauts de page par exemple.

II.4. TRANSFORMATION D'UNE EXPRESSION


REGULIERE EN AUTOMATE NON DETERMINISTE
• Entrée : expression régulière R sur un alphabet Σ.
• Sortie : un automate d'états finis non déterministe acceptant le langage
décrit par R.
Construction de Thompson :
Pour atteindre ce but, on utilisera la construction de Thompson qui
permet de procéder d'une manière incrémentale et simple. Dans cette
technique, on décompose l'expression régulière en composantes simples, on
construit leur automate et on compose ensuite les automates obtenus pour
atteindre l'automate final.
Règles de Thompson :
i) Pour l'expression ε on construit l'automate suivant :
ε

ii) Pour l'expression ε on construit l'automate suivant :

23
2. Analyse Lexicale

iii) Si r1 et r2 sont deux expressions régulières :


• Pour r1 | r2 on construit l'automate :

• Pour r1 . r2 on construit l'automate :

r1 r2

I1 F1I2 F2

• Pour r* on construit l'automate :

Propriétés de la construction de Thompson :


1. L'automate obtenu a un seul état final.
2. Il n'y a pas de transition entrante sur l'état initial.
3. Il n'y a pas de transition sortante de l'état final.

Application :
a. Construire l’AFN correspondant à l’expression régulière suivante par la
construction de Thompson : (a|b)* b b*
ε
a ε
3 4
ε ε ε ε ε
1 2 ε
7 8 9 10 11 12
b b
ε 5 6
b ε
ε

24
2. Analyse Lexicale

b. L’automate d’états finis déterministe correspondant à l’AFN précédent


est donné ci-après :
a b
{1,2,3,5,8}I {4,7,8,2,3,5} {6,9,7,8,2,3,5,10,12}
{4,7,8,2,3,5} {4,7,8,2,3,5} {6,9,7,8,2,3,5,10,12}
{6,9,7,8,2,3,5,10,12}F {4,7,8,2,3,5} {9,6,11,10,12,7,8,2,3,5}
{9,6,11,10,12,7,8,2,3,5} F {9,6,11,10,12,7,8,2,3,5} {9,6,11,10,12,7,8,2,3,5}
Pour plus de clarté, on renomme les états précédents par S1,S2,S3,S4 ce qui
donne la table de transition suivante :
a b
S1I S2 S3
S2 S2 S3
S3F S2 S4
S4 F S2 S4
Cet automate est schématisé ci-après :
a

a S2
a b
S1 a
b S4
b b
S3

c. Après application de l’algorithme de minimisation du nombre d’états de


l’AFD, on obtient l’automate suivant :

a
a b

S1,S2 S3,S4

25
2. Analyse Lexicale

II.5. CONTROLE PAR AFD vs. CONTROLE PAR


PROGRAMME

L’automate d’états finis déterministe est l’outil pratique pour effectuer


des contrôles lexicaux à partir de spécifications données. Cependant,
contrôler par AFD certaines spécifications est prohibitif du point de vue
espace mémoire occupé par la table de transition de l’AFD. D’autre part, on
ne peut contrôler par AFD certaines contraintes imposées sur les entités
lexicales. D’où la nécessité de recourir à un compromis entre ce qui doit être
contrôlé par AFD et ce qui doit être contrôlé par programme.
Exemple :
 On veut reconnaître les entités d’un langage L formées des lettres, des
chiffres et des tirets ‘-’. Un mot de L présente les caractéristiques
suivantes :
1. il doit commencer obligatoirement par une lettre et ne doit pas
finir par un tiret
2. il ne doit pas contenir 2 tirets consécutifs ni 2 chiffres
consécutifs
3. le nombre de chiffres est inférieur strictement au nombre de
lettres
4. la longueur d’un mot est comprise entre 2 et 50 caractères
i. Peut-on contrôler individuellement chacune des caractéristiques
précédentes par un AFD ?
 Les contraintes 1, 2 et 4 peuvent être contrôlées par AFD. Par contre, la
contrainte 3 ne peut pas être contrôlée par AFD.
ii. Peut-on contrôler la conjonction des caractéristiques précédentes par un
AFD ?
 On peut contrôler la conjonction des contraintes 1, 2, 3 et 4 par AFD.
iii. D'un point de vue pratique, quelles sont les caractéristiques qui doivent
être contrôlées par AFD et celles contrôlées par programme ?
 D’un point de vue pratique, on contrôle les contraintes 1 et 2 par AFD
et 3 et 4 par programme.

26
2. Analyse Lexicale

iv. Donner cet AFD que vous minimiserez.


 AFD minimal pour les contraintes 1 et 2 :

a-z

a-z a-z
e1 e2
0-9
a-z
0-9 e3
-
e4
-

v. Ecrire un programme d’analyse lexicale, en vous aidant de l'AFD


précédent pour analyser les mots du langage L.
 Programme d’analyse :
e= e1 ;
nbl=nbc=nbt=0;
c: = CarSuiv ;
Tant que (c ≠ '# ' et e ≠ ∅)
Faire switch (c)
Lettre : nbl++ ;
Chiffre : nbc++ ;
endswitch ;
nbt++;
e:= Transiter (e, c);
c:= CarSuiv();
Fait;
Si (e ∈ {e2,e3}) et (2≤nbt≤50) et (nbc<nbl)
Alors "Chaîne acceptée"
Sinon " Chaîne refusée"
Fsi

27
2. Analyse Lexicale

II.6. UTILISATION DE LEX DANS L'ANALYSE


LEXICALE

II.6.1. Introduction à LEX


LEX est un outil très populaire qui devient une norme de fait pour la
génération d’analyseurs lexicaux. Il prend en entrée les spécifications des
utilisateurs sous forme d’expressions régulières et génère (s’il n y a pas
d’erreurs) une programme en langage C. Ce programme en C contient la
table de transition de l’automate d’états finis déterministe et l’algorithme
d’analyse lexical. Ce programme en C devra à son tour compilé avec un
compilateur C pour générer l’exécutable de l’analyseur lexical.
Fichier.l

LEX

lex.yy.c *.c
routine
routines C
yylex()

Compilateur C Unix lib

Analyseur
Figure II.5. Génération d’analyseur lexical avec LEX

• Langage Lex :
√ permet de définir des expressions régulières. Il peut donc être utilisé
pour définir des unités lexicales qui sont spécifiées par des
expressions régulières.

 Programme Lex :
√ constitué d ’un ensemble d ’expressions régulières écrites dans le
langage Lex.
√ est mis dans un fichier avec l’extension .l (exemple : scan.l).
28
2. Analyse Lexicale

 Compilateur Lex :
√ génère l ’analyseur lexical à partir du programme Lex
√ exécuté à l ’aide de la commande lex (exemple : lex scan.l)
√ La commande lex génère le code C de l'analyseur qui est mis dans
un fichier lex.yy.c
√ Le fichier lex.yy.c est soumis au compilateur C pour générer le
code objet de l'analyseur
√ Peut être utilisé seul ou en conjonction avec YACC.

II.6.2. Expressions régulières de LEX


Les expressions régulières de LEX suivent une syntaxe stricte. Certains
caractères sont spéciaux et il faudrait faire très attention à l’écriture des
expressions régulières. Les expressions régulières LEX sont résumées dans
le tableau suivant :
Expression Signification Exemple
c tout caractère c qui n’est pas spécial a
\c caractère littéral c lorsque c est un métacaractère \+ \.
"s" chaîne de caractères "bonjour"
. n’importe quel caractère, sauf retour à la ligne a.b
^ l’expression qui suit ce symbole débute une ligne ^abc
$ l’expression précédant ce symbole en fin de ligne abc$
[s] n’importe quel caractère de s [abc]
[^s] tout caractère qui n’est pas dans s sauf fin ligne [^xyz]
r* 0, 1 ou plusieurs occurrences de r b*
r+ 1 ou plusieurs occurrences de r a+
r? 0 ou 1 occurrence de r d?
r{m} m occurrences de r e{3}
r{m,n} entre m et n occurrences de r f{2,4}
r1r2 r1 suivie de r2 ab
r1|r2 r1 ou r2 c|d
r1/r2 r1 si elle est suivie de r2 ab/cd
(r) r (a|b)?c
<x>r r si LEX se trouve dans l’état x <x>abc
Tableau II.2. Expressions régulières de LEX.

29
2. Analyse Lexicale

Caractères spéciaux (opérateurs) :


-"\[]^?.*+|()$/{}%<>
Exemples d’utilisation :
 [abc] reconnaît un (01) caractère a, b ou c
 [^abc] tout caractère sauf a, b et c
 Entre [ ], tous les caractères spéciaux sont ignorés sauf \ - et ^
 Si le caractère – doit être reconnu, il faut le mettre au début ou à la fin de
la suite de caractères. (exemple : [-+0-9] ; le premier – désigne le
caractère – tandis que le deuxième est utilisé pour désigner les chiffres
de 0 à 9).
 [a-zA-Z0-9] reconnaît un (01) caractère alphanumérique.
 ab ?c reconnaît ab ou abc
 [a-z]+ reconnaît toutes les chaînes de caractères en minuscules.
 [a-zA-Z] [a-zA-Z0-9]* reconnaît toutes les chaînes alphanumériques
commençant par une lettre.
 (ab|cd) est équivalente à ab|cd ; les parenthèses sont utilisées pour
grouper des expressions régulières.
 Que reconnaît (ab|cd+) ?(ef)*
 ab/cd reconnaît ab si suivi de la chaîne cd ; la chaîne cd est toujours sur
l’entrée à analyser.
 a{1,5} reconnaît une à cinq occurrences de a

II.6.3. Structure d'un programme LEX


Un programme Lex est constitué de trois sections séparées par %% :

Déclarations
%%
Règles de traduction
%%
Procédures auxiliaires

II.6.3.1. Section Déclarations


La section Déclarations peut, elle-même, se composer de :
 un bloc littéral
 des définitions
 des conditions de départ

30
2. Analyse Lexicale

i) bloc littéral
 commence par %{ et se termine par %}
 contient des déclarations et définitions en C
 est copié tel quel dans le fichier lex.yy.c produit par la commande
lex
 les définitions et déclarations qu’il contient sont globales au programme
produit par lex

 Exemple :
%{
#include "calc.h"
#include <stdio.h>
#include <stdlib.h>
%}

ii) définitions
 Associations d’identificateurs à des expressions régulières
 Permettent de compacter l'écriture des expressions régulières.

 Exemple :
separ [ \t\n]
espace {separ}+
lettre [A-Za-z]
chiffre [0-9]
ident {lettre}({lettre}|{chiffre})*
nbre {chiffre}+(\.{chiffre}+)?(E[+\-]?{chiffre}+)?

 Remarque :
Utilisation des noms d’expressions régulières déjà définies entre
accolades {}

iii) états d’analyse


 Les états d’analyse permettent de définir plusieurs états de Lex
 Lors de l'analyse d'un "chaîne" d'entrée Lex peut basculer d'un état à un
autre en fonction du contexte.

31
2. Analyse Lexicale

 Exemple :
%start etat1 etat2 ….
√ Où etat1, état2 … sont les états possibles de Lex
√ Le fonctionnement de Lex avec le basculement entre états sera
traité ultérieurement.

II.6.3.2. Section Règles de traductions


Contient deux parties :
Partie gauche :
 spécification des expressions régulières à reconnaître
 pour une chaîne de lettres et de chiffres, les guillemets peuvent être
omis
 identificateurs d’expressions régulières mis entre accolades
 Pour éviter certaines sources d'ambiguïtés commencer par les
expressions régulières spécifiques et écrire ensuite les expressions
régulières générales (par exemple, pour l'analyse d'un programme
commencer d'abord par énumérer les mots-clés du langage avant
spécifier les expressions régulières des identificateurs.
Partie droite :
 actions exécutées lorsque unités lexicales reconnues
 actions définies avec syntaxe C
 Si action est absente, Lex ignorera les caractères reconnus par
l’expression régulière correspondante.
 Si le caractère spécial | est mis dans la partie action alors l’action
pour cette règle est l’action pour la règle suivante.
 si l'analyseur est appelé par le l'outil YACC, alors :
√ les attributs de l’unité lexicale reconnue doivent être déposés
dans yylval
√ l ’unité lexicale reconnue doit être retournée
Remarque :
Le lexème reconnu est rangé dans le tableau externe yytext.
Exemple : Les deux lignes suivantes sont équivalentes. Elles affichent
toutes les chaînes alphabétiques reconnues.
[a-zA-Z]+ printf("%c",yytext);
[a-zA-Z]+ ECHO;

32
2. Analyse Lexicale

Cas d’ambiguïtés :
 En cas d’ambiguïté dans la reconnaissance (plusieurs chaînes peuvent
être reconnues par une même expression régulière) LEX choisit la plus
longue chaîne.
 Exemple :
o Expression ‘.*’ et si l’entrée est :
‘first’ quoted string here, ‘second’ here
o LEX reconnaît ‘first’ quoted string here, ‘second’
 Placer les cas spécifiques avant les cas généraux.
 Exemple :
o integer action pour mot-clé
o [a-z]+ identificateur

II.6.3.3. Section Procédures auxiliaires


 Section optionnelle qui permet de :
- définir toutes les fonctions utilisées dans les actions associées aux
expressions reconnues
- définir (si nécessaire) le programme principal

II.6.4. Quelques fonctions et variables de LEX


 L ’analyse lexicale est effectuée par une fonction yylex() qui est
contenue dans le fichier lex.yy.c
 La fonction yylex() qui doit être appelée pour utiliser l ’analyseur
lexical :
- analyse séquentiellement un fichier d ’entrée
- retourne 0 lorsqu ’elle rencontre une fin de fichier
- effectue des opérations spécifiées par le programme Lex, lorsqu
’une unité lexicale est reconnue
 LEX fournit un ensemble de variables et de routines :
- Variable yylval de LEX permet de communiquer des valeurs à YACC.
- yytext = variable contenant la chaîne de caractères courante qui a été
reconnue
- yyleng = longueur de la chaîne yytext
- yyless(k) = fonction admettant un entier comme argument
√ supprime les (yyleng-k) derniers caractères de yytext, dont la
longueur devient alors k

33
2. Analyse Lexicale

√ recule le pointeur de lecture sur le fichier d ’entrée de yyleng-k


positions, les caractères supprimés de yytext seront donc
considérés pour la reconnaissance des prochaines unités lexicales
- yymore() = fonction qui concatène la chaîne actuelle yytext avec celle
qui a été reconnue avant
- yywrap() = fonction appelée lorsque yylex() rencontre une fin de
fichier
√ si yywrap() retourne true, alors yylex() retourne 0 pour
indiquer une fin de fichier
√ si yywrap() retourne false, alors yylex() ignore la fin de
fichier et continue son analyse
√ Par défaut, yywrap() retourne 1, mais on peut la redéfinir.
 L ’entrée standard est notée yyin et la sortie standard est notée yyout

Exemple : Si on veut que :


- l ’entrée standard soit le fichier toto.e qui contiendra le texte
à analyser
- la sortie standard soit le fichier toto.s qui contiendra les
sorties (messages …) générés :
yyin = fopen(``toto.e``, ``r``);
yyout = fopen(``toto.s``, ``w``);



fclose(yyin);
fclose(yyout);

II.6.5. Exemple de programme Lex

II.6.5.1. Suppression des blancs et tabulations


Le programme LEX suivant efface tous les blancs et tabulations en fin de
ligne :
%%
[ \t]+$ ;
%%
main()
{
yylex() ;
}
34
2. Analyse Lexicale

Que fait le programme suivant ?


%%
[ \t]+$ ;
[ \t]+ printf("");
%%
main()
{
yylex() ;
}

II.6.5.2. Comptage de lettres et mots


Le programme LEX suivant comptabilise le nombre de mots et de caractères
d’un texte donné en entrée :
%{
int words, chars
%}
%%
[a-zA-Z]+ {words++ ;chars+=yyleng ;}
%%
main()
{
yylex() ;
}

II.6.5.3. Reconnaissance de nombres et identificateurs


%{
/* définitions des constantes littérales */
PPQ, PPE, EGA, DIF, PGQ, PGE,
SI, ALORS, SINON, ID, NB, OPREL
%}

/* définitions régulières */
delim [ \t\n]
bl {delim}+
lettre [A-Za-z]
chiffre [0-9]
id {lettre}+({lettre}|{chiffre})*
nombre {chiffre}+(\.{chiffre}+)?(E[+-]?{chiffre}+)?

35
2. Analyse Lexicale

%%
{bl} {/* pas d’action et pas de retour */}
si {return(SI);}
alors {return(ALORS);}
sinon {return(SINON);}
{id} {yylval = RangerId();return(ID);}
{nombre} {yylval = RangerNb();return(NB);}
"<" {yylval = PPQ;return(OPREL);}
"<=" {yylval = PPE;return(OPREL);}
"=" {yylval = EGA;return(OPREL);}
"<>" {yylval = DIF;return(OPREL);}
">" {yylval = PGQ;return(OPREL);}
">=" {yylval = PGE;return(OPREL);}

%%
RangerId()
{
/* procédure pour ranger dans la table des symboles l’entité lexicale dont le
premier caractère est pointé par yytext et dont la longueur est yyleng et
retourner un pointeur sur son entrée */
}

RangerNb()
{
/* procédure similaire pour ranger une entité lexicale qui est un nombre */
}

II.6.6. Utilisation des états d’analyse : exemple


 On désire interpréter l ’expression régulière [a-zA-Z]+ (un mot d ’au
moins une lettre) de deux manières, selon le contexte :
√ Contexte 1. Si mot entre guillemet, alors il représente une constante
alphanumérique (chaîne de caractères)
√ Contexte 2. Si mot pas entre guillemet, alors il est un identificateur

 On définit deux états const_alpha et normal, respectivement pour


les deux contextes. Ces états sont utilisés comme suit :
√ Initialement, état égal à 0

36
2. Analyse Lexicale

√ Tout au début, état mis à normal par l ’instruction BEGIN


normal
√ chaque fois qu ’on rencontre des guillemets, on commute d ’un état à
l ’autre (entre normal et const_alpha)
√ lorsqu ’on reconnaît une expression régulière [a-zA-Z]+, alors :
√ si l ’état courant est normal : alors un identificateur est reconnu
√ si l ’état courant est const_alpha : alors une chaîne de caractères
est reconnue

 Exemple :
%start normal const_alpha
%%
<normal>[a-zA-Z]+ {printf(``identificateur : % s \n``,yytext);}
<normal>\`` {BEGIN const_alpha;}
<const_alpha>[a-zA-Z]+ {printf(``chaine : %s \n``,yytext);}
<const_alpha>\`` {BEGIN normal;}
<normal,const_alpha>. { /* aucune action */ }
<normal,const_alpha>\n { /* aucune action */ }
%%
main()
{ yyin = fopen(``toto.e``, ``r``);
yyout = fopen(``toto.s``, ``w``);
BEGIN normal;
yylex(); /* yylex appelé une seule fois car pas de return */
flcose(yyin);
fclose(yyout);
}

37
2. Analyse Lexicale

II.7. EXERCICES

Exercice 1.
• Donner un automate d’états finis déterministe pour chacun des langages
suivants sur l’alphabet {0,1} :
a) l’ensemble de toutes les chaînes commençant par 1 et qui interprétées
comme la représentation binaire d’entiers sont divisibles par quatre.
b) Ensemble des chaînes de 0 et de 1 avec un nombre impair de 0 et un
nombre pair de 1.
c) Ensemble des chaînes de 0 et de 1 qui ne contiennent pas la sous-chaîne
011.

Exercice 2.
• Considérer l’expression régulière suivante :
(a|b)* a b*
a) Construire un automate d’états finis non déterministe pour l’expression
régulière précédente en utilisant la construction de Thompson.
b) Transformer cet automate d’états finis non déterministe en un automate
d’états finis déterministe.
c) Minimiser le nombre d'états de l'automate obtenu.

Exercice 3.
• Les entités lexicales d’un mini-langage de programmation sont les
suivantes :
Mots-clés begin, end, if, then, else
Identificateurs chaînes composées d’une lettre suivie de zéro ou
plusieurs lettres ou chiffres
Constantes chaînes composées d’un chiffre suivi de zéro ou
plusieurs chiffres
Opérateurs <, < =, =, < >, >, > =, +, -

a) Donner les expressions régulières qui décrivent ces entités lexicales


b) Construire un automate fini non déterministe pour ces expressions
régulières

38
2. Analyse Lexicale

Exercice 4.
• Les règles de la construction de Thompson transforment une expression
régulière R1 en un automate d’états finis N1. Proposer des règles
analogues de construction d’automates d’états finis non déterministes
pour les opérateurs suivants :
 R1+
 R1? (dont la signification est R1|εε)
• On modifie la règle de Thompson de construction de l'automate pour
l'expression R* en ne rajoutant pas un état initial et un état final (on
rajoute juste deux transitions étiquetées ε, l'une de l'état final de
l'automate de R vers l'état initial de cet automate et l'autre de l'état initial
vers l'état final). Cette règle est-elle toujours valable pour l'expression
R* ? Dans le cas général (composition de règles), la modification
proposée affecte-elle la validité des constructions ? Donner un exemple
concis pour justifier votre réponse.

Exercice 5.
• On veut reconnaître les entités d’un langage L formées des lettres
alphabétiques, des chiffres et des tirets ‘-’. Un mot de L présente les
caractéristiques suivantes :
• il doit commencer obligatoirement par une lettre;
• il doit contenir au moins 2 caractères;
• il ne doit pas contenir 2 tirets consécutifs ni 2 chiffres consécutifs;
• il ne doit pas finir par un tiret;
• le nombre de chiffres est inférieur au nombre de lettres;
• la longueur d’un mot n’excède pas 20 caractères.
a) Peut-on contrôler la longueur des mots par un automate ? Que doit-on
contrôler par automate et que doit-on contrôler par programme ?
b) Donner l’automate déterministe qui accepte les mots du langage L.
c) Ecrire un programme d’analyse lexicale en vous aidant d’un automate.

Exercice 6.
• Comment reconnaître les mots des langages suivants :
a) Toutes les chaînes de lettres contenant les voyelles (a, e, i, o, u) dans
l’ordre (ex : cradetillotum, aceitou).

39
2. Analyse Lexicale

b) Toutes les chaînes de lettres contenant des lettres par ordre croissant
dans l’alphabet (ex : city, not, bel).

Exercice 7.
• Ecrire un programme Lex qui :
1. convertit un texte écrit en majuscules en minuscules,
2. supprime les blancs et tabulations en fin de ligne,

Exercice 8.
• Ecrire un programme Lex qui a en entrée un fichier de nombres entiers
et qui en sortie ajoute la valeur 3 à tous les entiers divisibles par 7. Les
entiers non divisibles par 7 ne sont pas transformés.
Indication :
Utiliser la fonction atoi() qui convertit une chaîne de caractères "chiffres"
en sa valeur numérique.

Exercice 9.
a) Que reconnaît Lex avec l'expression régulière '.*' sur l'entrée suivante et
pourquoi ?
'first' quoted string here, 'second' here
b) Si on voulait reconnaître d'abord 'first', quelle expression régulière
définir ?

Exercice 10.
• Soit un texte comprenant des mots sur les différents caractères du code
ASCII. La taille des mots alphabétiques (composées des lettres de
l'alphabet {a,b,…, z}) ne dépasse pas 20 caractères.
Question :
• Ecrire un programme Lex qui donne l'histogramme des mots i.e. pour
chaque longueur de mot on donne le nombre de mots présents dans le
texte de cette longueur.

40
3. Principes de l'Analyse Syntaxique

CHAPITRE III. PRINCIPES DE


L'ANALYSE SYNTAXIQUE

III.1. INTRODUCTION
L'objectif d'une analyse syntaxique est de reconnaître si un
programme donné (le programme source dont les entités lexicales ont été
codées) appartient au langage engendré par une grammaire hors-contexte
(de type 2 dans la classification de Chomsky). On appelle également ces
grammaires des grammaires non-contextuelles ou en terme anglo-saxon des
grammaires Context-Free.
L'utilisation des grammaires non-contextuelles dans la phase
d'analyse syntaxique est motivée par le fait que les langages de
programmation actuels admettent tous des grammaires Context-Free pour
les générer. L'analyseur syntaxique pourra donc être "construit" de manière
efficace et automatique. L'automate à pile est de fait sous-jacent à toutes les
analyses syntaxiques.
Il est à préciser que si on pouvait obtenir une grammaire régulière
pour engendrer un langage de programmation, la phase d'analyse syntaxique
n'en sera que plus facilitée (utilisation d'automate d'états finis au lieu de
l'automate à pile) mais ce n'est pas le cas pour les langages de
programmation.
Avant d'étudier les méthodes d'analyses syntaxiques, il est utile de
faire quelques rappels sur les grammaires non-contextuelles et les langages
algébriques. Nous donnerons ensuite le principe général des méthodes
d'analyse syntaxique.

III.2. LES GRAMMAIRES NON-CONTEXTUELLES

Définition :
Une grammaire non-contextuelle G est un quadruplet <N,T,P,S,> où
 N : ensemble fini non vide de symboles appelés symboles non
terminaux,
41
3. Principes de l'Analyse Syntaxique

 T : ensemble fini de symboles appelés symboles terminaux,


 S : S ∈N, symbole de départ ou axiome de la grammaire,
 P : règles de réécritures ou ensembles de production,
Chaque production est de la forme :
A→α
où A ∈ N et α ∈ (N ∪ T)*

Exemple :
 La grammaire G suivante décrit les expressions arithmétiques :
√ G = <{E}, {+,-,*,/,(,),id}, P, E>
√ P: E→E+E
E→E-E
E→E*E
E→E/E
E→(E)
E → id
 L'ensemble des productions peut être réécrit de la manière suivante :
√ P : E → E + E | E – E | E * E | E / E | ( E ) | id

III.3. DERIVATIONS ET LANGAGES

III.3.1. Dérivations

III.3.1.1. Définition d'une dérivation


 Soit G une grammaire. On dit qu'une chaîne ω2 se dérive d'une chaîne
ω1 si ω2 s'obtient de ω1 par l'application d'une règle de production de
G. Cette dérivation est notée :
ω1 ⇒ ω2
√ Exemple :
A → β est une production de la grammaire,
α, γ ∈ (N ∪ T)*,
αAγ ⇒αβγ

42
3. Principes de l'Analyse Syntaxique

III.3.1.2. Suite de dérivations


Série de dérivations :
 Soient α1, α2, …, αm des chaînes appartenant à (N ∪ T)*, m ≥ 1 et :
α1 ⇒ α2
α2 ⇒ α3
:
αm-1 ⇒ αm
√ La suite de dérivation notée
α1 ⇒* αm
signifie que αm s'obtient de α1 par l'application de zéro, une ou plusieurs
règles de productions de G.

√ De façon analogue, la notation :


α1 ⇒+ αm
signifie que αm s'obtient de α1 par l'application de une ou plusieurs
règles de productions de G.

Dérivation la plus à gauche :


 Dans une suite de dérivation, si à chaque étape de dérivation une
production est appliquée au non-terminal le plus à gauche, la suite de
dérivation est dite la plus à gauche ou en terme anglais leftmost
derivation.

Dérivation la plus à droite :


 Dans une suite de dérivation, si à chaque étape de dérivation une
production est appliquée au non-terminal le plus à droite, la suite de
dérivation est dite la plus à droite ou en terme anglais rightmost
derivation.

Exemple :
 Considérer la grammaire suivante :
G = <{S,A}, {a,b}, P, S>
P : S → aAS | a
A → SbA | SS | ba

√ La chaîne aabbaa peut s'obtenir de l'axiome S en utilisant :

43
3. Principes de l'Analyse Syntaxique

i) La dérivation la plus à gauche :


S ⇒ aAS ⇒ aSbAS ⇒ aabAS ⇒ aabbaS ⇒ aabbaa
ii) La dérivation la plus à droite :
S ⇒ aAS ⇒ aAa ⇒ aSbAa ⇒ aSbbaa ⇒ aabbaa

III.3.2. Langages
 Etant donné une grammaire G, on appelle langage engendré par G et on
le note L(G) le langage :
L(G) = { ω | ω ∈ T* et S ⇒+ ω }
 Exemple :
Soit la grammaire G = <N,T,P,S,> avec :
N = {S} ; T = {a, b} ;
P = { S → aSb ; S → ab } ;
L(G) = { anbn | n ≥ 1 }

III.3.3. Arbre de dérivation


 Un arbre de dérivation également appelé arbre syntaxique est une
structure pour représenter une suite de dérivations.
 Soit G = <N,T,P,S>une grammaire non-contextuelle, un arbre de
dérivation est associé à une chaîne de L(G) et il est défini comme suit :
√ Chaque sommet de l'arbre a une étiquette qui est un symbole de N ∪
T ∪ {ε},
√ L'étiquette de la racine est le symbole S,
√ Les sommets non-feuilles de l'arbre ont des étiquettes appartenant à
N,
√ Les feuilles de l'arbre ont des étiquettes appartenant à T ∪ {ε},
√ Si un sommet d'étiquette A ∈ N a des fils d'étiquettes X1, X2, …, XN
(de gauche à droite) alors A → X1X2 …XN est une production de P.
Exemple :
 Considérer la grammaire suivante :
G = <{S,A}, {a,b}, P, S>
P : S → aAS | a
A → SbA | SS | ba
44
3. Principes de l'Analyse Syntaxique

√ La suite de dérivations suivante pour obtenir la chaîne aabbaa :


S ⇒ aAS ⇒ aSbAS ⇒ aabAS ⇒ aabbaS ⇒ aabbaa
√ peut être représenté par l'arbre suivant :
S
a A S

S b A a

a b a
Figure III.1. Arbre de dérivation.

III.3.4. Ambiguïté
 Une grammaire non-contextuelle est dite ambiguë si une chaîne ω (∈
T*) possède deux (ou plus) arbres de dérivation.
 Exemple :
√ Soit G = <{E}, {+,-,*,/,(,),id}, P, E>
√ P : E → E + E | E – E | E * E | E / E | ( E ) | id

√ La grammaire G précédente est ambiguë car la chaîne id + id * id


a deux arbres syntaxiques donnés ci-après :

E E
E + E E * E

id E * E E + E id

id id id id

Figure III.2. Arbres syntaxiques d'une même chaîne.

45
3. Principes de l'Analyse Syntaxique

III.4. TRANSFORMATIONS DE GRAMMAIRES NON-


CONTEXTUELLES

III.4.1. Propriétés particulières d'une grammaire


 Si un langage non-contextuel L est non vide, il peut être généré par une
grammaire non-contextuelle ayant les propriétés suivantes :
√ Chaque non-terminal et chaque terminal apparaît dans la dérivation
d'un mot de L.
√ Il n'y a pas de production unitaire (de la forme A → B où (A,B) ∈
N2).
√ Si ε ∉ L alors les ε-productions (de la forme A → ε où A ∈ N) ne
sont pas nécessaires.

III.4.2. Elimination des symboles inutiles


Symboles inutiles :
 Soit une grammaire G=<N,T,P,S> ; un symbole X de la grammaire G est
inutile s'il n'existe aucune dérivation de la forme :
S ⇒* αXβ ⇒* ω et ω ∈ T*
Processus d'élimination des symboles inutiles :
 Pour éliminer les symboles inutiles d'une grammaire G=<N,T,P,S>, on
applique les deux transformations suivantes dans l'ordre.
 Transformation 1 :
Transformer G en G'=<N',T,P',S> tel que pour chaque A ∈ N' on A
⇒*ω (ω ∈ T*) par l'algorithme suivant.
OLD_N = ∅;
NEW_N = {A | A → ω, ω ∈ T*};
While OLD_N NEW_N
Do Begin
OLD_N = NEW_N;
NEW_N = OLD_N ∪ {A | A → α, αω ∈ (T ∪ OLD_N)*;
End;
N' = NEW_N.

46
3. Principes de l'Analyse Syntaxique

 Transformation 2 :
Transformer une grammaire G=<N,T,P,S> en
G'=<N',T',P',S> tel que pour chaque X ∈ (N' ∪ T') ∃ α,
β ∈ (N' ∪ T') et S ⇒* αXβ.
Algorithme :
Placer S dans N'; S est non marqué;
While ∃ un non-terminal A non marqué dans N' et A → α1| α2 | …| αm
Do
Begin
ajouter tous les non-terminaux de α1, α2, …, αm à N';
ajouter tous les terminaux de α1, α2, …, αm à T';
End;
P' est l'ensemble des productions contenant des symboles de (N' ∪ T').

 Exemple
Elimination des symboles inutiles de la grammaire dont les productions
sont données ci-après:
S→A|B
A → aB | bS | ε
B → AB | BCc
C → AS | ε
Après l'application de la transformation 1, on obtient la grammaire dont
les productions sont données ci-après :
S→A
A → bS | ε
C → AS | ε
Après l'application de la transformation 2, on obtient la grammaire finale
sans symboles inutiles dont les productions sont données ci-après :
S→A
A → bS | ε

47
3. Principes de l'Analyse Syntaxique

III.4.3. Elimination des productions unitaires


Productions unitaires :
 Les productions unitaires sont des productions de la forme :
A→B
où (A,B) ∈ N2.

Elimination des productions unitaires :


Algorithme :
Si A → B où (A,B) ∈ N2 et A ≠ B
Alors Si B → β1 | β2 | … | βn
Alors Remplacer la production A → B par A → β1 | β2 | … | βn
FinSi ;
Si A → A où A ∈ N Alors supprimer cette production FinSi.

III.4.4. Rendre une grammaire ε-libre


Grammaire ε-libre
 Une grammaire est dite ε-libre si :
√ elle n’a pas de ε-productions
√ ou elle a exactement une production vide S → ε et que l’axiome S
n’apparaît pas en partie droite d’aucune production.

Rendre une grammaire G=<N,T,P,S> ε-libre


Eléments de réponse :
i) Déterminer les non-terminaux qui peuvent engendrer ε et les mettre
dans l'ensemble.
Si A → ε alors A ∈ Nε ;
Si B → α et symboles de α dans Nε alors B ∈ Nε.
ii) Construire les productions P' de la grammaire ε-libre G' :
Si A → X1 X2 … Xn est dans P
Alors Ajouter toutes les productions A → α1 α2 … αn à P'
où Si Xi ∉ Nε alors αi = Xi; FinSi;
Si Xi ∈ Nε alors αi = (Xi ou ε ) FinSi;

48
3. Principes de l'Analyse Syntaxique

iii) Si (S ∈ Nε)
Alors Si S n'apparaît dans un aucun membre droit de production
Alors Ajouter à P' la production S → ε
Sinon Créer symbole Z (nouvel axiome) et ajouter production Z→S|ε
FinSi;
FinSi;

III.4.5. Substitution des non-terminaux


Lemme 1
 Soit G=(N,T,P,S) une grammaire de type 2 (Context-Free).
 Soient A→α1Bα2 une production de P et B→ β1| β2| …βr l’ensemble des
B-productions (productions de P où B apparaît dans un membre gauche
de production).
 Soit G1=(N,T,P1,S) obtenue en supprimant la production A→α1Bα2 de P
et en ajoutant les productions A→α1β1α2|α1β2α2 …|α1βrα2 pour obtenir
l’ensemble des productions P1.
 Alors L(G) = L(G1)

Lemme 2
 Soit G=(N,T,P,S) une grammaire de type 2 (Context-Free).
 Soient A→Aα1|Aα2|…|Aαn les A-productions où A est le terme le plus à
gauche des MDP (Membre Droit de Production).
 Soient A→ β1| β2| …βs les autres A-productions.
 Soit G1=(N∪{A’},T,P1,S) obtenue en remplaçant les A-productions par :
A→βi | βiA’ (1 ≤ i ≤ s) et A’→ αj | αjA’ (1 ≤ j ≤ n)
 Alors L(G) = L(G1)

Remarques :
 Deux grammaires différentes générant exactement le même langage sont
dites équivalentes.
 Lors de l’analyse syntaxiques, une grammaire peut avoir de "meilleures"
propriétés qu’une grammaire équivalente.

49
3. Principes de l'Analyse Syntaxique

III.5. PRINCIPE DES ANALYSES DESCENDANTES ET


ASCENDANTES
Le but de l'analyse syntaxique d'un programme est de vérifier si ce
programme appartient au langage de programmation c'est à dire s'il existe
une suite de dérivations depuis l'axiome de la grammaire (non-contextuelle)
jusqu'à aboutir à ce programme.
L'analyse doit être "déterministe" i.e. lorsque l'analyseur syntaxique
s'arrête, soit le programme est syntaxiquement correct ou il existe une erreur
dans ce programme.
Il existe deux grandes classes de méthodes pour effectuer l'analyse
syntaxique : les analyses descendantes et les analyses ascendantes.

Analyse descendante :
 On part de l'axiome de la grammaire pour retrouver le programme
source en effectuant des dérivations successives. Si on se place dans
l'arbre syntaxique représentant le programme source, cette stratégie
revient à partir de la racine (l'axiome) et à descendre vers les feuilles
représentant les symboles terminaux.
Analyse ascendante :
 On part du programme source pour retrouver l'axiome de la grammaire
en effectuant des dérivations successives inverses. On va cette fois partir
des feuilles de l'arbre syntaxique pour remonter vers la racine.
 Exemple
Soit la grammaire G dont les productions sont données ci-après et la
chaîne ω=abba à analyser syntaxiquement :
S → aA | bB
A → aBS | bS
B → bB | a
√ Le processus d'analyse descendante de la chaîne ω=abba peut illustré
par :
S ⇒ aA ⇒ abS ⇒ abbB ⇒ abba
√ Le processus d'analyse ascendante de la chaîne ω=abba peut illustré
par :
abba ⇐R abbB ⇐R abS ⇐R aA ⇐R S
où ⇐R désigne est une dérivation inverse

50
3. Principes de l'Analyse Syntaxique

III.6. BACKUS–NAUR FORM (BNF)


Backus–Naur Form (BNF) ou forme de Backus–Naur est une méta-
syntaxe utilisée pour décrire les grammaires non contextuelles (context-
free). Cette forme a été développée par John Backus et Peter Naur.
BNF est très utilisée pour la description des grammaires des langages
de programmation. Beaucoup de générateurs de compilateurs ou
d’analyseurs syntaxiques s’inspirent de BNF pour l’introduction des
grammaires cibles.

Définition :
 Une spécification BNF est un ensemble de règles de dérivation définie
par :
<symbole> ::= expression1 | expression2 | … | expressionN

<symbole> est un non-terminal, et expressioni est une suite de symboles de
la grammaires (terminaux et non-terminaux et éventuellement le mot vide)
Les symboles qui n’apparaissent jamais en membre gauche de production
sont des terminaux. Les symboles non terminaux sont toujours entre < >

Exemple :
<Expression> ::= <Expression> "+" <Terme>
| <Expression> "-" <Terme>
| <Terme>
<Terme> ::= <Terme> "*" <Facteur>
| <Terme> "/" <Facteur>
| <Facteur>
<Facteur> ::= "(" <Expression> ")"
| "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" |

51
3. Principes de l'Analyse Syntaxique

III.7. EXERCICES

Exercice 1.
a) Ecrire un programme en langage C qui permet de rendre une grammaire
ε–libre.
b) Rendre la grammaire G = <{S}, {a,b}, P, S> ε-libre. Les règles
productions P sont données ci-après.
P : S → aSbS | bSaS | ε
Exercice 2.
• Considérer la grammaire G=(N,T,P,S) dont les productions sont données
ci-après :
P : <Instr> → if <Condition> then <Instr> else <Instr>
| if <Condition> then <Instruction>
| begin <LI> end
| i
<Condition> → c
<LI> → <LI> ; i | i
a) Montrer que la grammaire G précédente est ambiguë en donnant deux
arbres syntaxiques pour le fragment de programme suivant :
if c then if c then i else i.
b) Comment pourra-t-on faire une analyse syntaxique "déterministe" pour
ce type d'instructions (instructions conditionnelles if).

Exercice 3.
 Soit la grammaire G=({S,A,B},{a,b,c},P,S).
P: S → aSA | cA
A → cA | a
B → bB | b
a) Eliminer les symboles inutiles de la grammaire précédente.
b) Donner le langage engendré par la grammaire épurée.
c) Donner les dérivations les plus à gauche de la chaîne : aacaaca.
d) Donner les dérivations les plus à droite de la chaîne : aacaaca.

52
3. Principes de l'Analyse Syntaxique

Exercice 4.
• Une grammaire G = (T, N, P, S) sous forme normale de Chomsky
(FNC) est une grammaire dont toutes les productions sont de la
forme :
A → BC ; A → a
où a est un terminal et A, B, C sont des non terminaux
a) Montrez que toute grammaire G de type 2 peut être une grammaire sous
FNC.
b) Transformez la grammaire suivante en grammaire sous FNC :
S→A/B
A → aB / bS / ε
B → AB / BCc
c) Quel peut être l'intérêt de l'utilisation des grammaires sous FNC en
analyse syntaxique ?

Exercice 5.
 Une grammaire G = (T, N, P, S) sous forme normale de Greibach (FNG)
est une grammaire dont toutes les productions sont de la forme :
A → aα α , α ∈ N*
où a est un terminal et α une chaîne de non terminaux
a) Montrez que toute grammaire G de type 2 peut être une grammaire sous
FNG.
b) Transformez les grammaires suivantes en grammaire sous FNG :
S → Ab / b / Sa
A → a / AS / Sb

Exercice 6.
 Soit la grammaire des expressions arithmétiques G=({E},{+,-
,*,/,(,),i},P,E) dont les productions sont données ci-après (le terme i
représente un identificateur) :
P: E → E + E | E - E | E * E | E / E | (E) | i
a) Montrer que la grammaire précédente est ambiguë en donnant deux
arbres syntaxiques pour la chaîne : i + i * i.
b) "Interpréter" les deux arbres syntaxiques dans le sens "comment va être
évaluée l'expression suivante : i + i * i ? "

53
3. Principes de l'Analyse Syntaxique

c) Construire une grammaire des expressions arithmétiques G’non ambiguë


qui prend qui donne aux opérateurs les priorités et les associativités
suivantes :
√ + et – ont même priorité et sont associatifs à gauche ;
√ * et / ont même priorité et sont associatifs à gauche ;
√ * et / sont plus prioritaires que + et - ;
√ les expressions "parenthésées" sont évaluées en premier.
d) Construire l'arbre syntaxique pour l'expression i + i * i. Est-ce que
l'évaluation de cette expression se fera correctement ?.

54
4. Analyse syntaxique descendante

CHAPITRE IV. ANALYSE SYNTAXIQUE


DESCENDANTE

IV.1. INTRODUCTION

On étudiera les analyses syntaxiques descendantes "déterministes"


c'est à dire sans retour arrière. Lors de l'arrêt d'un analyseur syntaxique de ce
type, soit l'entrée à analysé est syntaxiquement correcte soit l'entrée n'est pas
acceptée i.e. syntaxiquement incorrecte. Nous allons étudier les deux
principales méthodes que sont l'analyse syntaxique dite "LL" et l'analyse
syntaxique par descente récursive.
Dans les analyses descendantes, on débute avec l'axiome de la
grammaire qui engendre les mots du langage à analyser, et on utilise
plusieurs dérivations pour aboutir à l'entrée à analyser (si le programme
source est écrit correctement selon une certaine grammaire).
Les méthodes descendantes sont simples à implémenter, mais la classe
des grammaires de type 2, avec lesquelles on peut construire ces analyseurs,
n'est pas très grande. Les méthodes ascendantes dites "LR" (détaillées dans
la prochain chapitre) admettent un ensemble de grammaires plus large.

IV.2. ANALYSE SYNTAXIQUE LL

Définition du terme LL(k) :


 LL(k) : Left to right scanning using the Leftmost derivation taking
decision by reading k tokens from the input stream.
 La traduction de ce terme anglais conduit à la définition suivante :
Analyser le flot d'entrée de gauche à droite en utilisant la dérivation la
plus à gauche après lecture de k items du flot d'entrée.

55
4. Analyse syntaxique descendante

Remarque :
 Le nombre k est dans la majorité des cas égal à 1, car pour si k > 1
l'analyse devient moins intéressante. C'est pour cette raison que nous
détaillerons l'analyse LL(1) et nous survolerons l'analyse LL(k).

IV.2.1. Analyse syntaxique LL(1)

IV.2.1.1. Ensembles DEBUT et SUIVANT


Avant de voir ce qu'est une grammaire LL(1), nous introduisons les
notions d'ensembles DEBUT et SUIVANT associés aux non terminaux
d'une grammaire.
Ensemble DEBUT :
• Soit G une grammaire non contextuelle G=<N,T,S,P> et X un non-
terminal appartenant à N, alors :
DEBUT(X) = {a | X ⇒* a.α, a ∈ T et α ∈(T ∪ N)*}.
Si X ⇒* ε alors ε ∈ DEBUT(X)
Calcul de l'ensemble DEBUT :
• L'algorithme suivant montre comment calculer un ensemble DEBUT :
DEBUT(X)
i) Si X est un terminal alors DEBUT(X) = X ;
ii) Si X → ε alors ajouter à DEBUT(X) ;
iii) Si X → Y1Y2…Yn
 Si ε ∉ DEBUT(Y1) on ajoute DEBUT(Y1) à DEBUT(X) ;
 Si ε ∈ DEBUT(Y1), …, DEBUT(Yi-1) avec 2 ≤ i ≤ n
Alors ajouter DEBUT(Y1), …, DEBUT(Yi) ε exclu à DEBUT(X)
 Si ε ∈ DEBUT(Y1), DEBUT(Y2), …, DEBUT(Yn)
Alors ajouter ε à DEBUT(X)

Remarque:
 Ces règles sont appliquées jusqu'à ce qu'aucun terminal ni ε ne puisse
être ajouté aux ensembles DEBUT.

56
4. Analyse syntaxique descendante

Ensemble SUIVANT :
• Soit G une grammaire non contextuelle G=<N,T,S,P> et X un non-
terminal appartenant à N, alors :
SUIVANT(X) = {a | S ⇒* αXaβ, a ∈ T ∪ {#} et α,β ∈(T ∪ N)*}.
Calcul de l'ensemble SUIVANT :
• L'algorithme suivant montre comment calculer un ensemble SUIVANT :
SUIVANT(X)
i) Mettre # (qui est le marqueur de fin de l'entrée à analyser) dans
SUIV(S) où S est l'axiome de la grammaire ;
ii) S'il y a une production A → αXβ
Alors ajouter DEBUT(β) sauf ε à SUIVANT(X) ;
iii) S'il y a une production A → αX
ou une production A → αXβ avec ∈ DEBUT(β)
Alors ajouter SUIVANT(A) à SUIVANT(X) ;
Remarque:
 Ces règles sont appliquées jusqu'à ce qu'aucun terminal ni # ne puisse
être ajouté aux ensembles SUIVANT.
Exemple 1 :
 Calculer les ensembles DEBUT et SUIVANT pour les non-terminaux de
la grammaire dont les productions sont données ci-après :
S → aSSA | ε
A → aSb | b
B → bB | ε

DEBUT SUIVANT
S aε #ab
A ab #ab
B bε #ab
Exemple 2 :
 Calculer les ensembles DEBUT et SUIVANT pour les non-terminaux de
la grammaire dont les productions sont données ci-après :

57
4. Analyse syntaxique descendante

S → ABSb | ε
A → aBb | b
B → bB | cS | ε

DEBUT SUIVANT
S abε #ba
A ab bca
B bcε ab

IV.2.1.2. Grammaires LL(1)


Définition :
 Une grammaire G = <N,T,S,P> est LL(1) si et seulement si pour toute
paire de règles A → α | β les conditions suivantes s'appliquent :
√ Pour aucun terminal a, α et β ne se dérivent toutes les deux en des
chaînes commençant par a.
√ Une des chaînes au plus α ou β se dérive en la chaîne vide.
√ Si β ⇒* ε , α ne se dérive pas par un terminal de SUIVANT(A) et
vice-versa.
Remarque :
 Les trois conditions précédentes peuvent être résumées par la formule ci-
après. Une grammaire G = <N,T,S,P> est LL(1) si et seulement si pour
toute paire de règles A → α | β on a :

DEBUT(α.SUIVANT(A)) ∩ DEBUT(β.SUIVANT(A)) = ∅

58
4. Analyse syntaxique descendante

IV.2.1.3. Table d'analyse


• Donnée : Une grammaire non-contextuelle G.
• Résultat : Une table d'analyse M pour G. Les indices des lignes de cette
table sont les non terminaux de la grammaire et les indices des colonnes
sont les terminaux de la grammaire plus le caractère spécial # (# est le
marqueur de fin du flot à analyser).
Algorithme :
 L'algorithme suivant construit une table d'analyse M pour une grammaire
donnée G :
i) Pour chaque production A → α, procéder aux étapes ii) et iii) ;
ii) Pour chaque terminal a dans DEBUT(α)
Ajouter la règle A → α à M[A,a] ;
iii) Si ε ∈ DEBUT(α)
Alors ajouter la règle A → α à M[A,b] pour b ∈ SUIVANT (A);
iv) Faire de chaque entrée non définie "une erreur".
Propriétés :
 Si une grammaire vérifie les conditions LL(1) alors sa table d'analyse est
mono-définie c'est à dire chaque entrée de la table contient au plus une
règle de production de la grammaire.
 Si une table d'analyse d'une grammaire G est mono-définie alors G est
LL(1).
 Si une grammaire G est LL(1) alors G est non ambiguë.
 Une grammaire G LL(1) permet de faire une analyse syntaxique
descendante sans retour arrière. Au cours de toute dérivation, il existera
au plus une possibilité pour remplacer un non terminal par une de ses
parties droites selon le caractère courant de l'entrée à analyser.

IV.2.1.4. Programme d'analyse


 Le programme d'analyse LL(1) permet de faire une syntaxique
déterministe si la table d'analyse associée à une grammaire est mono-
définie. Ce programmera analysera le flot d'entrée, produit un flot de
sortie en utilisant la table d'analyse et une pile explicite.
 Le schéma de l'analyseur LL(1) est décrit ci-après :

59
4. Analyse syntaxique descendante

…a+b# Tampon d'entrée

Pile X
Y Programme Flot de Sortie
Z d'analyse
#

Table
d'analyse M
Figure IV.1. Schéma d'un analyseur LL(1).

Algorithme d'analyse :
i) Initialement la pile contient # et au-dessus l'axiome de la grammaire ;
ii) Soit X le symbole en sommet de pile et a le symbole d'entrée courant ;
iii)Si X = a = #, l'analyseur s'arrête et annonce la réussite finale de l'analyse
;
iv) Si X est un terminal ≠( a = #), l'analyseur s'arrête et signale une erreur ;
v) Si (X = a) ≠ #, l'analyseur enlève X de la pile et avance son pointeur de
flot d'entrée sur le symbole suivant ;
vi) Si X est un non-terminal
Alors consulter l'entrée de la table M[X,a]
Si M[X,a] = {A → α}
l'analyseur enlève X du sommet de la pile ;
et empile les symboles de α de droite à gauche
(si α = UVW alors empiler W, V et U dans cet ordre) ;
Si M[X,a] = "erreur" l'analyseur appelle une procédure de récupération
sur erreur.
vii) Aller à iii)

60
4. Analyse syntaxique descendante

IV.2.1.5. Exemple d'analyse LL(1)


√ Considérer la grammaire G = <{E,E',T,T',F},{+,*,(,),id}, E, P> dont les
règles de production sont données ci-dessous :
E → T E'
E' → + T E' | ε
T → F T'
T' → * F T' | ε
F → (E) | id
√ Calcul des ensembles DEBUT et SUIVANT :

DEBUT SUIVANT
E ( id #)
E' +ε #)
T ( id +#)
T' *ε +#)
F ( id *+#)
√ Table d'analyse pour la grammaire précédente :

+ * ( ) id #
E E → T E' E → T E'
E' E' → + T E' → ε E' → ε
E'
T T → F T' T → F T'
T' T' → ε T' → * F T' → ε T' → ε
T'
F F → (E) F → id
Tableau IV.1. Table d'analyse LL(1).

√ La grammaire précédente est LL(1).


√ Analyse de la chaîne a + b * c #.

61
4. Analyse syntaxique descendante

Contenu Pile Restant de chaîne à Action


(sommet pile à droite) analyser
#S id + id * id # Remplacer S par E' T
# E' T id + id * id # Remplacer T par T' F
# E' T' F id + id * id # Remplacer F par id
# E' T' id id + id * id # avancer
# E' T' + id * id # Dépiler T'
# E' + id * id # Remplacer E' par E' T +
# E' T + + id * id # avancer
# E' T id * id # Remplacer T par T' F
# E' T' F id * id # Remplacer F par id
# E' T' id id * id # avancer
# E' T' * id # Remplacer T' par T' F *
# E' T' F * * id # avancer
# E' T' F id # Remplacer F par id
# E' T' id id # avancer
# E' T' # Dépiler T'
# E' # Dépiler E'
# # Chaîne acceptée

IV.2.1.6. Récupération sur erreur


Définition :
 La récupération sur erreur est la tentative de poursuivre l'analyse
syntaxique si une erreur se produit et ne pas s'arrêter à la première erreur
détectée.
 On détecte une erreur au cours d'une analyse LL(1) lorsque :
√ le terminal en sommet de pile ne correspond pas au symbole
d'entrée courant ou,
√ un non terminal A est en sommet de pile, le symbole d'entrée est
a et M[A,a] est vide.
Récupération sur erreur en mode "panique" :
 La récupération des erreurs dans le mode panique est fondée sur l'idée de
sauter les symboles du flot d'entrée jusqu'à ce qu'apparaisse une entité

62
4. Analyse syntaxique descendante

lexicale appartenant à un ensemble sélectionné d'unités lexicales de


synchronisation :
√ SUIVANT du non terminal en sommet de pile ;
√ ; dans les langages comme C ou Pascal ; …

IV.2.1.7. Transformation de grammaires


 Il n'existe pas de procédé pour rendre une grammaire G LL(1). Mais par
contre, certaines conditions sont nécessaires mais non suffisantes pour
qu'une grammaire G soit LL(1). Ces conditions sont les suivantes :
√ La grammaire doit être factorisée à gauche ;
√ La grammaire ne doit pas être récursive gauche.
Factorisation à gauche d'une grammaire :
 Des productions non factorisées à gauche d'une grammaire du type :
A → α β 1 | α β 2| … | α β n | γ
où α ≠ ε
 sont remplacées par les productions suivantes :
A → α A' | γ
A' → β1 | β2| … | βn

récursivité gauche d'une grammaire :


 Une grammaire est dite récursive à gauche si elle contient un non
terminal A tel que :
A ⇒*A α
où est une chaîne quelconque.

Elimination de la récursivité à gauche immédiate :


 Les règles sous la forme suivante :
A → A α1 | A α2| … | A αm | β1 | β2| … |
βn
où aucun β ne commence par A
 sont remplacées par les productions suivantes :
A → β1 A'| β2 A'| … | βn A'
A' → α1 A'| α2 A' | … | αm A' | ε

63
4. Analyse syntaxique descendante

 Cette transformation n'élimine pas la récursivité gauche indirecte. Par


exemple, cette transformation n'a aucun effet sur la récursivité gauche
indirecte de la grammaire dont les productions sont données ci-après :
S→Aa|b
A→Ac|Sd| ε

Elimination de la récursivité à gauche indirecte :


 L'algorithme suivant, élimine systématiquement, les récursivités gauches
(directes et indirectes) d'une grammaire. Il fonctionne correctement si la
grammaire est sans cycle (dérivations A ⇒+ A et sans production vide
(A → ε).
Ordonner les non terminaux A1, A2, …, An ;
Pour i := 1 to n
Faire Pour j := 1 to i – 1
Faire
Remplacer chaque production de la forme Ai → Aj γ
par les productions Ai → δ1 γ | δ2 γ | … | δk γ
où Aj → δ1 | δ2 | … | δk sont les productions Aj courantes
Fait ;
Eliminer les récursivités gauches immédiates des Ai productions ;
Fait.

 Exemple : éliminer le récursivité gauche de la grammaire dont les


productions sont données dans ce qui suit :
S→Ab|b|Sb
A → a | AS | S b
√ Ordre S, A

64
4. Analyse syntaxique descendante

√ Etape 1:
Elimination de la récursivité gauche directe
S → A b S' | b S'
S' → b S' | ε
√ Etape 2:
Substitution
A → a | A S | A b S' b | b S' b
Elimination de la récursivité gauche directe
A → a A' | b S' b A'
A' → S A' | b S' b A' | ε

IV.2.2. Analyse syntaxique LL(k)

L'analyse syntaxique LL(k) avec > 1 est une généralisation de l'analyse


LL(1). Elle consiste à "voir" obligatoirement k caractères du flot d'entrée
avant de prendre une décision d'analyse. Cette méthode est peu utilisée.
Nous résumerons ci-après les principaux changement a effectuer sur les
notions définis précédemment pour faire une analyse LL(k).

IV.2.2.1. Ensembles DEBUT-k et SUIVANT-k


Ensemble DEBUT-k :
• Soit G une grammaire non contextuelle G=<N,T,S,P> et X ∈ (T ∪ N)*
alors :
DEBUT-k (X) = {
ω | X ⇒* ω.α, ω ∈ T+, |ω| = k et α ∈(T ∪ N)* ou
ω | X ⇒* ω, ω ∈ T+, |ω| ≤ k et α ∈(T ∪ N)*
};
Si X ⇒* ε alors ε ∈ DEBUT-k(X)

65
4. Analyse syntaxique descendante

Ensemble SUIVANT-k :
• Soit G une grammaire non contextuelle G=<N,T,S,P> et X un non-
terminal appartenant à N, alors :
SUIVANT(X) = { ω| S ⇒* αXωβ,
où ω ∈ (T ∪ {#})k, |ω| = k, et α,β ∈(T ∪ N)*}.

IV.2.2.2. Grammaires LL(k)


Définition :
 Une grammaire G = <N,T,S,P> est LL(k) si et seulement si pour toute
paire de règles A → α | β les conditions suivantes s'appliquent :

DEBUT-k(α.SUIVANT-k(A)) ∩ DEBUT-k(β.SUIVANT-k(A)) = ∅

IV.2.2.3. Table d'analyse


Algorithme :
 L'algorithme suivant construit une table d'analyse LL(k) M pour une
grammaire donnée G :
i) Pour chaque production A → α, procéder aux étapes ii) et iii) ;
ii) Ajouter la règle A → α à M[A, ω]
pour tout ω ε DEBUT-k(α.SUIVANT-k(A));
iii)Si ε ∈ DEBUT-k(α)
Alors ajouter la règle A → α à M[A, ω] pour ω ∈ SUIVANT-k (A) ;
iv) Faire de chaque entrée non définie "une erreur".

IV.2.2.4. Programme d'analyse


 Le programme d'analyse LL(k) est légèrement modifié par rapport au
programme d'analyse LL(1). On avancera dans le flot d'entrée toujours
entité par entité. Mais pour prendre une décision d'analyse quand un non
terminal est en sommet de pile, il faudra "voir" k entités du flot d'entrée
i.e. consulter la table d'analyse LL(k).

66
4. Analyse syntaxique descendante

IV.3. ANALYSE SYNTAXIQUE PAR DESCENTE


RECURSIVE

Définition :
 L'analyse syntaxique par descente récursive n'est en fait que la version
récursive (au sens implémentation informatique) de l'analyse LL(1).
C'est à dire qu'au lieu de manipuler la pile explicitement, celle-ci sera
gérée implicitement lors des appels. L'analyseur (programme d'analyse)
est constitué d'une suite de procédures.

IV.3.1. Conditions préalables à une descente récursive

 Pour faire une analyse syntaxique par descente récursive pour analyser
les mots d'un langage L(G), la grammaire G doit vérifier les conditions
LL(1) i.e.
 Pour toute paire de règles de G tel que A → α | β on a :

DEBUT(α.SUIVANT(A)) ∩ DEBUT(β.SUIVANT(A)) = ∅

IV.3.2. Ecriture des procédures

 Soit G une grammaire vérifiant les conditions LL(1). Les étapes suivantes
montrent le principe d'écriture l'ensemble des procédures de l'analyseur :
 A chaque non terminal de la grammaire correspond une procédure ;
 On ajoute la règle de production suivante : Z → S # où S est l'axiome de
la grammaire et # le marqueur de fin de chaîne.
 On utilisera les variables tc et ts pour désigner, respectivement, le
symbole courant du flot d'entrée à analyser et son symbole suivant dans
ce flot ;

67
4. Analyse syntaxique descendante

 Soit A un non terminal de la grammaire tel que A → α1 | … | αn


Le corps de la procédure A est définie comme suit :
si tc ∈ DEBUT(αi.SUIVANT(A)) alors Traiter l'entrée αi ;
Déclencher une erreur si tc ∉ DEBUT(αi.SUIVANT(A)), i=1..n ;
Le traitement d'un membre droit d'une règle de production A→αi se fait
comme suit :
Chaque terminal en partie droite est comparé au symbole courant de
l'entrée :
si égalité lire le symbole suivant
sinon déclencher l'impression d'une erreur ;
Chaque non-terminal en partie droite conduit à son appel.

IV.3.3. Exemple d'analyse

 Considérer la grammaire G =<{S,A},{a,b,c},S,P> dont les productions


sont données ci-après :
S→aAb|ε
A → c A | ab
 On ajoute la règle suivante :
Z→S#
 Calcul des ensembles DEBUT et SUIVANT :

DEBUT SUIVANT
S aε #
A ca b
 La grammaire G précédente vérifie les conditions LL(1).
 Ecriture des procédures :
Procédure Z( )
Début
S( ) ;
Si tc ='#"
Alors "Chaîne syntaxiquement correcte"
Sinon "Erreur"
FinSi ;
Fin.
68
4. Analyse syntaxique descendante

Procédure S( )
Début
Si tc = 'a'
Alors
tc = ts ;
A( ) ;
Si tc = 'b'
Alors tc = ts
Sinon "Erreur"
FinSi ;
FinSi ;
Fin.
Procédure A( )
Début
Si tc = 'c'
Alors
tc = ts ;
A( ) ;
Sinon
Si tc = 'a'
Alors tc = ts
Si tc = 'b'
Alors tc = ts
Sinon "Erreur"
FinSi
Sinon "Erreur"
FinSi
FinSi
Fin.

69
4. Analyse syntaxique descendante

 Analyse de la chaîne suivante : a c c a b b #

Contenu Pile_Implicite Restant de chaîne à Action


(sommet pile à droite) analyser
Z accabb# appel S
ZS accabb# avancer ; appel A
ZSA ccabb# avancer ; appel A
ZSAA cabb# avancer ; appel A
ZSAAA abb# avancer ; avancer ;
retour A
ZSAA b# retour A
ZSA b# retour A
ZS b# avancer ; retour S
Z # "Chaîne Correcte"

 Analyse de la chaîne suivante : a c a a b b #

Contenu Pile_Implicite Restant de chaîne à Action


(sommet pile à droite) analyser
Z acaabb# appel S
ZS acaabb# avancer ; appel A
ZSA caabb# avancer ; appel A
ZSAA aabb# avancer ; "Erreur"
ZSAA abb# "Chaîne Incorrecte"

70
4. Analyse syntaxique descendante

IV.4. EXERCICES

Exercice 1.
 Soient les grammaires G1 et G2 suivantes :
G1. S → aASb / bBSa / a

A → aA / ε

B → bB / abS

G2. S → abSAb / c / aAabS

A → aA / ε

√ G1 et G2 sont-elles LL(1)? LL(k)?

Exercice 2.
• Eliminer la récursivité à gauche dans les grammaires suivantes :
S → Aa / SSb / ε

A → Ba / Sb

B → Ab / Bba / a

Exercice 3.
• On définit la grammaire G=(N,T,P,S) suivante :
P: <bloc> → programme <LD> début <LI> fin

<LD> → <LD> ; d | d

<LI> → <LI> ; i | i

a) Ecrire l’algorithme d’analyse syntaxique qui reconnaît les mots du


langage L(G) par la méthode de la descente récursive.
b) Analyser les chaînes suivantes :
• programme d ; d début i fin
• programme d début d i fin

71
4. Analyse syntaxique descendante

Exercice 4.
• On définit la grammaire G=(N,T,P,S) suivante :
P: <Instr> → if <Condition> then <Instr> else <Instr>
| if <Condition> then <Instr>
| begin <LI> end
| i
<Condition> → c
<LI> → <LI> ; i | i
c) La grammaire G précédente est-elle LL(1) ? Que doit-on faire pour
essayer de transformer la grammaire G en grammaire LL(1) ? Votre
grammaire transformée est–elle LL(1) ? Pourquoi ?
d) Construire la table d'analyse LL(1) pour votre grammaire transformée.
e) Lever la multidéfinition de votre table d'analyse en utilisant la
convention du langage Pascal pour le traitement des instructions if
imbriquées.
f) Analyser la chaîne suivante : if c then if c then i else i.

Exercice 5.
• On définit la grammaire G=(N,T,P,S) suivante :
P: S → Aa | b
A → Ac | Sd |e
a) Ecrire l’algorithme d’analyse syntaxique qui reconnaît les mots du
langage L(G) par la méthode de la descente récursive.
b) Analyse la chaîne suivante : e adca

Exercice 6.
• On définit la grammaire Gi,j=({S,A,B},{a,b,c},P,S). i et j sont des entiers
positifs ou nuls.

P: S → aSbia | bA
A → bBb | ε
B → cB | bjc

72
4. Analyse syntaxique descendante

a) La grammaire Gi,j est-elle LL(k) ?

b) Ecrire l’algorithme d’analyse syntaxique par la méthode de la descente


récursive pour la grammaire Gi,j (On dispose de la fonction Read(n) qui
permet de lire le nième caractère de l'entrée à partir du caractère courant).
c) Pour la grammaire G1,1, analyser la chaîne suivante : abba

Exercice 7.
 En s'inspirant de la méthode LL, on désire écrire un compilateur utilisant
une méthode descendante pour un langage de programmation en arabe.
Le compilateur est écrit dans un langage classique (C par exemple) et à
chaque étape de l'analyse on utilise la dérivation la plus à gauche.
a) Quel nom pourrais-t-on donner à la méthode d'analyse si on s'inspire
de la signification de LL? Quelles conditions doit vérifier la
grammaire pour faire une analyse descendante déterministe sans
retour arrière ?
 On désire maintenant modifier le compilateur précédent pour qu'à
chaque étape de l'analyse on utilise la dérivation la plus à droite.
b) Quel nom pourrais-t-on donner à la méthode d'analyse si on s'inspire
de la signification de LL? Quelles conditions doit vérifier la
grammaire pour faire une analyse descendante déterministe sans
retour arrière ?
c) Appliquer les conditions et transformations décrites précédemment
pour faire une analyse descendante déterministe (construction de la
table d'analyse) reconnaissant les mots du langage engendrés par la
grammaire suivante :
G: E → T+E|T
T → T*F|F
F→ (E)|γ
a) Analyser la chaîne : 3γ * 2γ + 1γ et donner son arbre syntaxique.

73
4. Analyse syntaxique descendante

Exercice 8.
• Soit la grammaire G=({E},{,∧,∨,(,),i},P,E) :
P: E →  E | E ∧ E | E ∨ E | (E) | i

a) G est-elle LL(1) ? LL(k) ?


b) Construire une grammaire non ambiguë G’ pour les expressions
logiques.
c) Construire la table d'analyse LL(1) pour votre nouvelle grammaire. Cette
grammaire est-elle LL(1) ?

74
5. Analyses syntaxiques ascendantes

CHAPITRE V. ANALYSES
SYNTAXIQUES ASCENDANTES

INTRODUCTION
L'analyse syntaxique ascendante est également appelée analyse par
décalage/ réduction. Dans ces analyses on tente de remonter vers l'axiome
de la grammaire depuis le programme d'entrée à analyser.
Nous abordons ce chapitre par les méthodes les plus simples pour
terminer avec la méthode la plus "élaborée".

V.1. ANALYSE PAR PRECEDENCE D'OPERATEURS

V.1.1. Grammaire d'opérateurs


 Une grammaire est d'opérateur si elle ne contient pas des règles de la
forme :
√ A→ε,
√ A → … XY … où (X, Y) ∈ N2

V.1.2. Relations de précédence d'opérateurs


 On définit des relations appelées relations de précédence d'opérateurs
uniquement entre symboles terminaux de la grammaire. Ces relations
seront notées =, < et > et sont définies dans ce qui suit.
Soit une grammaire G = <N, T, S, P> :
 a = b s'il existe une règle tel que :
√ A → … ab … ou A → … aXb …
√ (X, A) ∈ N2, (a, b) ∈ T2
 a < b s'il existe une règle tel que :
√ A → … aX … et (X ⇒+ b… ou X ⇒+ Yb …
√ (A, X, Y) ∈ N2, (a, b) ∈ T2

75
5. Analyses syntaxiques ascendantes

 a > b s'il existe une règle tel que :


√ A → … Xb … et (X ⇒+ …a ou X ⇒+ … aY
√ (A, X, Y) ∈ N2, (a, b) ∈ T2

V.1.3. Grammaire de précédence d'opérateurs


Définition :
 Une grammaire G est une grammaire de précédence d'opérateurs si :
i) G est une grammaire d'opérateurs ;
ii) Il existe au plus une relation de précédence entre deux symboles
terminaux de la grammaire.
Exemple :
 La grammaire G dont les productions sont données dans ce qui suit est-
elle de précédence d'opérateurs ?
E→E+T|T
T→T*F|F
F → (E) | i
 Relations de précédence après rajout de la règle Z → #E# :
√ Relations tirées de la règle E → E + T :
+>+ +<*
*>+ +<i
i>+ +<(
)>+
√ Relations tirées de la règle T → T * F :
*>* *<i
i>* *<(
)>*
√ Relations tirées de la règle F → ( E ) :
(<+ +>) (=)
(<* *>) (<i
i>) (<( )>)
√ Relations tirées de la règle Z → # E # :
#<+ # < (+ # < i #<*
+># )># * > # i > #)

76
5. Analyses syntaxiques ascendantes

√ Toutes les relations de précédence précédentes sont résumées dans la


tableau suivant :

+ * i ( ) #
+ > < < < > >
* > > < < > >
i > > > >
( < < < < =
) > > > >
# < < < <
Tableau V.1. Table des relations précédence d'opérateurs.

√ La grammaire G précédente est de précédence d'opérateurs.

V.1.4. Algorithme d'analyse de précédence d'opérateurs


L'algorithme suivant montre comment on procède pour effectuer une
analyse de précédence d'opérateurs :
La pile contient initialement #
Analyse de la chaîne ω #
ETIQ : Si (Sommet pile = '#' et tc= '#')
Alors chaîne acceptée
Sinon soit a symbole en sommet- pile
et soit b symbole courant
Si (a=b) ou (a<b)
Alors empiler (relation)
empiler (b)
tc←ts
Sinon Si (a>b)
Alors Tant que Sommet-pile ≠ <
Faire Dépiler
Fait
Dépiler
Sinon Erreur
FinSi
FinSi
FinSi
Aller à ETIQ.

77
5. Analyses syntaxiques ascendantes

Remarques :
i) Pour pouvoir faire une analyse de précédence d'opérateurs, on a
imposé des restrictions sur les grammaires. Don la classe de grammaires
qui admettent une telle analyse est restreinte.
ii) Les non terminaux de la grammaire sont implicitement pris en
compte dans l'analyse de précédence d'opérateurs

V.1.5. Exemple d'analyse


Analyser par la méthode de précédence d'opérateurs si la chaîne suivante
: i * (i + i) appartient au langage de la grammaire G des expressions
arithmétiques donnée dans les sections précédentes :
Pile Relation Restant de la Action
(sommet à droite) chaîne à analyser
# < i * (i + i) # empiler ; empiler
#<i > * (i + i) # dépiler ; dépiler
# < * (i + i) # empiler ; empiler
#<* < (i + i) # empiler ; empiler
#<*<( < i + i) # empiler ; empiler
#<*<(<i > + i) # dépiler ; dépiler
#<*<( < + i) # empiler ; empiler
#<*<(<+ < i) # empiler ; empiler
#<*<(<+<i > )# dépiler ; dépiler
#<*<(<+ > )# dépiler ; dépiler
#<*<( = )# empiler ; empiler
#<*<(=) > # 4 fois dépiler
#<* > # dépiler ; dépiler
# # Chaîne accepté

V.2. ANALYSE PAR PRECEDENCE SIMPLE


 Cette analyse est une généralisation de l'analyse par précédence
d'opérateurs car elle intègre également les non terminaux dans son
analyse.

78
5. Analyses syntaxiques ascendantes

V.2.1. Relations de précédence simple


Définitions des ensembles PREMIER et DERNIER :
√ PREMIER (A) = { X | A ⇒+ X α, X ∈ (N ∪ T), α ∈ (N ∪ T)* }
√ DERNIER (A) = { X | A ⇒+ αX, X ∈ (N ∪ T), α ∈ (N ∪ T)* }
Relations entre symboles :
 S1 = S2
√ Si ∃ une production A →…S1S2…dans G (S1,S2) ∈ (NUT)²
 S1<S2
√ Si ∃ une production A →…S1X…et S2 ∈ PREMIER(X)
 S1>S2
√ Si ∃ une production A →…XY…et S1 ∈ DERNIER (x) et S2 ∈
Début (Y) S2 ∈ T
S S S

…X… … S1 X … …XY…

… S1 S2 … S2 …….. … S1 S2 …
S1 = S2 S1 < S2 S1 > S2

V.2.2. Grammaire de précédence simple


 Une grammaire G est de précédence simple si elle vérifie les conditions
suivantes :
i) Il existe au plus une relation de précédence simple entre symboles de la
grammaire ;
ii) Deux règles de production ne peuvent pas avoir un même membre droit :
iii)Les productions vides ne sont pas permises.

Théorème :
 Si une grammaire G est de précédence simple alors G est non ambiguë.

V.2.3. Algorithme d'analyse de précédence simple


L'algorithme suivant montre comment on procède pour effectuer une
analyse de précédence simple la chaîne ω #:

79
5. Analyses syntaxiques ascendantes

La pile contient initialement #


ETIQ : Si (Sommet pile = '#' et tc= '#')
Alors "chaîne acceptée"
Sinon soit a symbole en sommet- pile et soit b symbole courant
Si (a=b) ou (a<b)
Alors empiler (relation) ;
empiler (b) ; tc←ts ;
Sinon Si (a>b)
Alors Tant que Sommet-pile ≠ <
Faire Dépiler ;
Fait
Dépiler ;
/* les éléments dépilés correspondent à MGP tel que
X→MGP */
Empiler le non terminal X précédé de la relation
Existante entre le sommet de pile et ce non terminal ;
Sinon Erreur
FinSi
FinSi
FinSi
Aller à ETIQ.

V.2.4. Exemple d'analyse


 La grammaire G dont les productions sont données dans ce qui suit est-
elle de précédence simple ?
S→A)
A → ( | Aa | AS
 Relations de précédence après rajout de la règle Z → #S# :
√ Relations tirées de la règle S → A ) :
A=)
DERNIER(A) > )
{(, a, S, ) } > )

80
5. Analyses syntaxiques ascendantes

√ Relations tirées de la règle A → Aa :


A=a
DERNIER(A) > a
{(, a, S, ) } > )
√ Relations tirées de la règle A → AS :
A=S
DERNIER(A) > (
{(, a, S, ) } > (
A < PREMIER(S)
A < {A, (}
√ Relations tirées de la règle Z → # S # :
# < PREMIER (S)
DERNIER (S) > #
√ Toutes les relations de précédence simples sont résumées dans la tableau
suivant :

S A ( ) a #
S > > >
A = < < = =
( > > >
) > > >
a > > >
# < <
Tableau V.2. Table des relations de précédence simple.

√ La grammaire G précédente est de précédence simple.


√ Analyser par la méthode de précédence simple si la chaîne suivante :
(a(a))# appartient au langage de la grammaire G des expressions
arithmétiques donnée dans les sections précédentes :

81
5. Analyses syntaxiques ascendantes

Pile Relation Restant de la Action


(sommet à droite) chaîne à analyser
# < (a(a))# Décaler
#<( > a(a))# Réduction : A → (
#<A = a(a))# Décaler
#<A=a > (a))# Réduction : A → Aa
#<A < (a))# Décaler
#<A<( > a))# Réduction : A → (
#<A<A = a))# Décaler
#<A<A=a > ))# Réduction : A → Aa
#<A<A = ))# Décaler
#<A<A=) > )# Réduction : S → A)
#<A=S > )# Réduction :A → AS
#<A = )# Décaler
#<A=) > # Réduction : S → A)
#S # Chaîne acceptée

V.2.5. Optimisation
Remarque :
 L'opération d'optimisation concernant l'analyse par précédence simple
décrite ci-après concerne la réduction de l'espace mémoire nécessaire
pour stocker la table des relations. Cette table sera d'une certaine
manière compactée.
Principe :
 Au lieu de garder la table des relations, on associera à chaque symbole
de la grammaire a, deux valeurs numériques désignées par fa et ga. Les
valeurs f et g seront choisies de telle sorte que si a < b (resp. =, >) dans
la table des relations alors fa < gb (resp. =, >). Au cours de l'analyse par
précédence simple, on testera alors la relation entre f(sommet_pile) et
g(terme_courant).
Avantage de l'utilisation des valeurs f et g :
 Avec les entiers f et g on utilisera 2n cases mémoires alors qu'avec la
table des relations n2 cases mémoires sont nécessaires. En terme de
temps d'exécution, l'utilisation des valeurs f et g engendrera une
82
5. Analyses syntaxiques ascendantes

exécution plus rapide si la table des relations (grande taille) n'est que
partiellement en RAM alors la table des valeurs f et g (petite taille) est
entièrement en RAM.
Désavantage de l'utilisation des valeurs f et g :
 Avec les entiers f et g, il existera toujours une relation entre le sommet
de pile et le terme courant. De ce fait, la détection des erreurs sera un
peu différée.
Comment trouver les valeurs fa et ga pour chaque symbole a de G ?
Etape 1 : Construction du graphe des symboles f et g
Parcourir la table des relations :
 Si a = b alors alors fa et gb appartiennent à un même sommet du
graphe. S'il n'a pas de relation d'égalité, chaque symbole f ou g sera
un sommet distinct du graphe.
 Si a < b alors tracer un arc partant du groupe de gb vers le groupe de
fa.
 Si a > b alors tracer un arc partant du groupe de fa vers le groupe de
gb.
Etape 2 : Obtenir les valeurs des symboles f et g
 Si le graphe des symboles contient un circuit alors pas de valeurs
possibles pour f et g.
 Si le graphe ne contient pas de circuit alors :
- associer à fa la longueur du plus grand chemin commençant par le
groupe de fa.
- associer à ga la longueur du plus grand chemin commençant par le
groupe de ga.
Comment exploiter efficacement le graphe pour obtenir les valeurs
de f et g :
Tri topologique sur les sommets du graphe (algorithme linéaire) :
i) Utiliser un compteur initialisé à 0 ;
ii) Chercher les sommets du graphe n'ayant pas d'arcs sortants ;
iii) Attribuer la valeur du compteur aux symboles de ces sommets ;
iv) Supprimer ces sommets du graphe avec tous les arcs qui y entrent
;
v) Incrémenter le compteur ;
vi) Si graphe "non vide" alors aller à ii).

Exemple :
83
5. Analyses syntaxiques ascendantes

 Remplacer la table des relations de la section 2.4 par une table de valeurs
f et g.
√ Graphe des symboles f et g :

fS gS ga g)

gA

fS fa f(
f)

g( f#
g#

Figure V.1. Graphe des symboles f et g.


√ Valeurs f et g :
S A ( ) a #
f 2 0 2 2 2 0
g 0 1 1 0 0 0

V.2.6. Transformations de grammaires simples


Remarque :
 On peut transformer aisément certaines grammaires pour qu'elles soient
de précédence simple. Ces grammaires sont celles vérifiant toutes les
conditions que doit satisfaire une grammaire de précédence simple sauf
des cas de multidéfinitions <= ou >=.
Cas X < = Y :
X = Y i.e. ∃ A ∈ N | A → αXYβ, (α,β) ∈ (N ∪ T)*
X < Y i.e. ∃ B ∈ N | B → α'XZβ', (α',β') ∈ (N ∪ T)* et Y ∈
PREMIER(Z)
Transformation pour éliminer la relation = :
Transformer la règle A → αXYβ en A → αXA' et A' → Yβ

84
5. Analyses syntaxiques ascendantes

Cas X > = a :
X = a i.e. ∃ A ∈ N | A → αXaβ, (α,β) ∈ (N ∪ T)* et a ∈ T
X > a i.e. ∃ B ∈ N | B → α'ZYβ', X ∈ DERNIER(Z) et a ∈
DEBUT(Y)
Transformation pour éliminer la relation = :
Transformer la règle A → αXaβ en A → A'aβ et A' → αX
Remarque :
 Il faudra s'assurer après ces transformations qu'on a pas deux MDP
(membres droits de production) identiques.

V.3. ANALYSE PAR PRECEDENCE FAIBLE

V.3.1. Grammaires de précédence faible


Définition:
 Le calcul des relations est identique au calcul des relations par la
méthode de précédence simple mais il faut remplacer la relation de
précédence = par <.
 Une grammaire G est de précédence faible si :
√ Dans la table des relations, il n'y pas de multidéfinitions ;
√ Quand il existe deux règles A → xαy et B → y :
- Il n'existe pas de relation entre α et B, α ∈ (NUT), (x,y) ∈
(NUT)*
- Il n'existe pas de productions vides
- Deux règles distinctes ne peuvent pas avoir un même membre
droit de production.

V.3.2. Analyse par précédence faible


Principe :
 L'analyse par faible précédence est semblable à l'analyse par précédence
simple sauf qu'au moment d'effectuer une réduction : on cherche le MDP
de plus grande longueur en partant du sommet de pile.
 Cette manière de procéder est imposée par la condition :
√ Quand il existe deux règles A → xαy et B → y :
- Il n'existe pas de relation entre α et B, α ∈ (NUT), (x,y) ∈
(NUT)*
85
5. Analyses syntaxiques ascendantes

V.3.3. Exemple d'analyse


 La grammaire G dont les productions sont données dans ce qui suit est-
elle de précédence simple ? faible ?
S → bAdS | aa
A → aS | a | d
 G est-elle de précédence simple ? faible ?
√ Il n'existe pas de productions vides.
√ Il n'existe pas deux MDP identiques.
√ Table des relations :

S A b d a #
S > >
A =
b = < <
d = < > <
a = < > <= >
# < <
Tableau V.3. Table des relations.

√ La grammaire G précédente n'est de précédence simple.


√ Transformation de = en < ; la grammaire G est de précédence faible car
elle vérifie toutes les conditions de précédence faible :
√ Analyser par la méthode de précédence faible si la chaîne suivante : aa#
appartient au langage de la grammaire G des expressions arithmétiques
donnée dans les sections précédentes :
Pile Relation Restant de la Action
(sommet à droite) chaîne à analyser
# < aa# Décaler
#a < a# Décaler
#aa > # Réduction : S → aa
#S # Chaîne acceptée

86
5. Analyses syntaxiques ascendantes

Remarques :
 Lors d'une analyse par précédence faible, il est inutile d'empiler le
symbole < car c'est la seule relation qui peut être présente dans la pile.
 Si on devait ignorer la dernière condition pour qu’une grammaire soit de
précédence faible alors on aura des incohérences lors de l’analyse.
L’exemple suivant montre un tel cas.
 Soit G une grammaire dont les productions sont :
o S → BA
o A → Ba | a
o B→b
 Si on forçait l’analyse de la chaîne "ba" avec l’algorithme de
précédence faible on aura un blocage alors que la chaîne "ba"
appartient à L(G).

V.3.4. Optimisation
Remarque :
 L'opération d'optimisation est similaire à celle décrite dans l'analyse
précédence simple.
Comment trouver les valeurs fa et ga pour chaque symbole a de G ?
Etape 1 : Construction du graphe des symboles f et g
Parcourir la table des relations :
 Chaque symbole f ou g sera un sommet du graphe.
 Si a < b alors tracer un arc partant du sommet gb vers le sommet fa.
 Si a > b alors tracer un arc partant du sommet fa vers le sommet gb.
Transformation du graphe :
 Chercher des sommets indépendants et les regrouper (2 sommets
sont indépendants s'il n'existe aucun chemin d'un sommet à l'autre);
 Aucun groupe ne doit contenir des successeurs et des prédécesseurs
d'un autre groupe.
Etape 2 : Obtenir les valeurs des symboles f et g
 Si le graphe des symboles contient un circuit alors pas de valeurs
possibles pour f et g.
 Si le graphe ne contient pas de circuit alors :
- associer à fa la longueur du plus grand chemin commençant par le
groupe de fa.
- associer à ga la longueur du plus grand chemin commençant par le
groupe de ga.
87
5. Analyses syntaxiques ascendantes

Remarque :
 L'opération de constitution des groupes sera faite de telle sorte à pouvoir
le plus possible regrouper des symboles f et g dans un même groupe (ces
symboles auront alors une même valeur numérique). Le but de cette
opération est d'accélérer la détection des erreurs (on a réintroduit des
relations = alors qu'elles n'existent pas dans l'analyse par précédence
faible).

V.4. ANALYSE PAR LA METHODE LR(K)


Signification de LR (k) :
L : Left to right parsing, constructing a
R : Rightmost derivation in reverse, using
k : k tokens to take an analysis decison.
Ce qui donne après traduction :
Analyse de gauche à droite qui construit la dérivation la plus à droite
dans l'ordre inverse utilisant k symboles pour prendre une décision
d'analyse.
Propriétés de l'analyse LR (k) :
 Méthode par décalage/réduction sans rebroussement la plus générale
connue ;
 La classe des grammaires qui peuvent être analysées par la méthode LR
est un sur-ensemble strict de la classe des grammaires qui peuvent être
analysées par les méthodes prédictives (grammaires LL) ;
 Un analyseur LR peut détecter aussitôt que possible une erreur de
syntaxe.
Remarques :
i) k est généralement égal à 1.
ii) Les méthodes LR sont les méthodes les plus utilisées par les
générateurs de compilateurs ; Yacc par exemple utilise la méthode
LALR(1) pour générer l'analyseur syntaxique.

V.4.1. Analyse Contextuelle

Dans cette section, nous montrons les fondements de l'analyse LR et


comment nous pourrions construire un analyseur LR. Toutefois, la méthode
dite "contextuelle" n'est pas pratique pour la génération automatique
d'analyseurs LR. Les méthodes pratiques seront données dans la section 5.2.
88
5. Analyses syntaxiques ascendantes

V.4.1.1. Contexte d'une règle de production


Définition :
Soit G = <N,T,S,P> une grammaire non contextuelle ; on rajoute la règle
Z → S#k (pour simplifier l'écriture de l'analyseur, # étant le marqueur de
fin de chaîne, Z devenant le nouvel axiome de la grammaire) ; le couple
θ,ω
(θ ω) est un contexte de la règle A → α de G si et seulement si :
 Z ⇒+ βAω ⇒ βαω (dérivation droite) ;
 θ = βα est dit contexte gauche (contenu de la pile au cours de
l'analyse) ;
 ω est appelé contexte droit (ce qui reste à analyser de la chaîne
d'entrée) ;
 ω ∈ (T ∪ {#}) ; (α,β)∈ (T ∪ N)*.
+

Exemple :
 Donner les contextes gauches et droits des règles de production de la
grammaire G=<{S,A,B},{a,b},S,P> dont les productions sont données
ci- après :
Règle Contexte gauche Contexte Droit
S → AB AB #
A → aA a*aA b+ #
A→a a*a b+ #
B → bB Ab*bB #
B→b Ab*b #

V.4.1.2. Contexte LR(k) d'une règle de production


Définition :
Soit G = <N,T,S,P> une grammaire non contextuelle :
si (θ,ω) est un contexte de la règle A → α de G
alors θ.DEBUTk(ω ω) est un contexte LR(k) de la règle A → α
Exemple :
 Les contextes LR(1) des règles de production de la grammaire
précédente sont données par le tableau suivant :
Règle Contexte LR(1)
S → AB AB#
A → aA a*aAb
A→a a*ab
B → bB Ab*bB#
B→b Ab*b#
89
5. Analyses syntaxiques ascendantes

V.4.1.3. Grammaires LR(k)


Définition :
Soit G = <N,T,S,P> une grammaire non contextuelle, G est une
grammaire LR(k) si et seulement si :
si θ est un contexte LR(k) de la règle A → α de G
et θ.ω est un contexte LR(k) de la règle B → β de G
avec θ ∈ (T ∪ N)*, ω ∈ (T ∪ {#})*, (A,B) ∈ (N)2; (α,β)∈ (T ∪ N)*
alors ω=ε, A=B et α=β
Interprétation :
 Avec un contenu de pile et à la lecture de k symboles de prévision, il ne
peut y avoir qu'une action possible parmi : un décalage, une réduction,
une erreur ou une acceptation au cours de l'analyse LR(k).
Exemples :
i) La grammaire donnée précédemment n'est pas LR(0) car :
aa est un contexte LR(0) (i.e. un contexte gauche) de la règle A → a
a est un contexte LR(0) (i.e. un contexte gauche) de la même règle A→a
ii) La même grammaire est cependant LR(1) car :
Il n'existe pas deux contextes LR(1) distincts identiques ou un préfixe de
l'autre.
iii)La grammaire G dont les productions sont données ci-après est-elle
LR(1) ? LR(k) ?
S → Ab | Bc
A → Aa | ε
B → Ba | ε

Règle Contexte gauche Contexte Droit


S → Ab Ab #
S → Bc Bc #
A → Aa Aa a*b#
A→ε pile vide a*b#
B → Ba Ba a*c#
B→ε pile vide a*c#
La grammaire G précédente n'est pas LR(k) ∀k ≥ 1 car :
ak est un contexte LR(k) de A → ε et
ak est un contexte LR(k) de B → ε.

90
5. Analyses syntaxiques ascendantes

Théorèmes :
i) Si une grammaire G est LR(k) alors G est LR(k+1).
ii) Si une grammaire G est LL(k) alors G est LR(k).

V.4.1.4. Principe de l'analyse LR(k)


Dans l'analyse LR(k) on empile des symboles terminaux (on parle
alors de décalage) ou non terminaux (on a remplacé des symboles terminaux
par un non terminal) pour avoir dans la pile un contexte gauche de la
grammaire. Si un contexte gauche d'une règle A → α est présent en pile
alors on pourra effectuer une réduction i.e. remplacer dans la pile α par A à
la "vue" de k symboles de l'entrée à analyser.

Remarques :
 Si la condition donnée en définition (section 5.1.3) n'est pas vérifiée
alors peut y avoir une indécision sur l'action à entreprendre avec un
contenu de pile et k symboles de prévision.
 Si par exemple un contexte LR(k) est préfixe d'un autre contexte LR(k)
alors il y aura une indécision entre un décalage et une réduction. Quand
le plus contexte gauche est présent en pile doit-on effectuer une
réduction ou attendre que le plus grand contexte gauche soit en pile
(donc faire au moins un décalage supplémentaire) pour faire la réduction
?
Comment construire un analyseur LR(k) :
i) Trouver les contextes LR(k) de la grammaire ;
ii) Construire un automate d'états finis déterministe qui reconnaît les
contextes LR(k) ;
iii) Transcrire cet automate en table d'analyse ;
iv) Algorithme d'analyse qui utilise cette table pour décider des actions à
entreprendre.
Exemple :
√ La grammaire dont les productions sont données ci-après est LR(1) (voir
section 5.1.3) ; les productions sont numérotées pour les désigner avec
ces numéros :
(1) S → AB
(2) A → aA
(3) A → a
(4) B → bB
(5) B → b

91
5. Analyses syntaxiques ascendantes

√ Automate reconnaissant les contextes LR(1) de la grammaire


précédente; arriver à un état final (grisé sur le schéma suivant) indique
qu'un contexte LR(1) a été reconnu ; les états finaux sont numérotés par
#
1 Accept

A B #
0 2 3 R(1
)
b
b # R(5
)
a 6 B
a #
7 R(4
)
b
4 R(3
)

A
b
5 R(2
)
les numéros de règles correspondent aux contextes LR(1).
Figure V.2. Automate reconnaissant les contextes LR(1).

Comment construire la table M d'analyse LR(1) à partir de


l'automate :
√ La table a comme indices des lignes les états non-terminaux ;
√ Les indices des colonnes sont les symboles de la grammaire et le
caractère # ;
√ Si dans l'automate existe une transition d'un état non final E1 vers un
état non final E2 sur un terminal t alors remplir M[E1, t] = "Décaler E2"
(ou en plus bref "D E2") ;
√ Si dans l'automate existe une transition d'un état non final E1 vers un
état non final E2 sur un non terminal A alors remplir M[E1, A] = "E2" ;
√ Si dans l'automate existe une transition d'un état non final E1 vers un
état final R(i) sur un terminal t alors remplir M[E1, t] = "Réduire par la
règle i" (ou en plus bref "R (i)") à la vue du terminal t (t n'est pas décalé
sur la pile);
√ M[E1, #] = "Accepter" si le contexte LR(1) reconnu est S# où S est
l'axiome de la grammaire.

92
5. Analyses syntaxiques ascendantes

Table d'analyse LR(1) de l'exemple précédent :


a b # S A B
0 D4 - - 1 2 -
1 - - Accepter - - -
2 - D6 - - - 3
3 - - R (1) - - -
4 D4 R (3) - - 5
5 - R (2) - - - -
6 - D6 R (5) - - 7
7 - - R(4) - - -
Tableau V.4. Table d'analyse LR(1).

V.4.1.5. Algorithme d'analyse LR


 Le programme d'analyse LR permet de faire une syntaxique déterministe
si la table d'analyse, associée à une grammaire, est mono-définie. Ce
programmera analysera le flot d'entrée, produit un flot de sortie en
utilisant la table d'analyse et une pile. La table d'analyse organisée en
deux parties : ACTION et SUCCESSEUR.
 Le schéma de l'analyseur LR est décrit ci-après :

…a+b# Tampon d'entrée

Pile Sn
Xn Programme Flot de
Sn-1 d'analyse LR Sortie

S0

ACTION SUCCESSEUR

Figure V.3. Schéma d'un analyseur LR(1).

Remarques :
93
5. Analyses syntaxiques ascendantes

 La partie ACTION de la table d'analyse correspond à la sous table où les


indices des colonnes sont des terminaux. La partie SUCCESSEUR
(fonction de transfert) de la table d'analyse correspond à la sous table où
les indices des colonnes sont les non terminaux.
 La pile est utilisée pour ranger des chaînes de la forme S0X1S1X2 …
XnSn (sommet de pile à droite) où Xi est symbole de la grammaire et Si
est un état.
 L'état en sommet de pile et le symbole d'entrée courant suffisent pour
décider de l'action d'analyse à entreprendre.
Algorithme d'analyse LR(1) :

La pile contient au départ l'état initial de l'automate S0 ;


Le pointeur de chaîne Ps pointe sur le premier symbole de la chaîne à
analyser ;
Répéter indéfiniment
Début
Soit S l'état en sommet de pile ;
Soit a le symbole pointé par Ps ;
Si ACTION[S,a] = "Décaler T"
Alors empiler(a) ; empiler(T) ;
avancer Ps sur le prochain symbole ;
Sinon Si ACTION[S,a] = "Réduire par A → α"
Alors dépiler 2*|α| symboles ;
Soit U le nouvel état en sommet de pile ;
empiler (A);
empiler(SUCCESSEUR[U,A];
Sinon Si ACTION[S,a] = "Accepter"
Alors retourner (Réussite());
Sinon retourner (Echec());
FinSi ;
FinSi ;
FinSi ;
Fin.

94
5. Analyses syntaxiques ascendantes

Remarque :
 Le programme d'analyse LR est le même pour toutes les analyses. Seules
les tables d'analyses changent.

Exemple :
 En utilisant la table d'analyse de la section 5.1.4 analyser les chaîne
"aaabb" et "aabab" pour vérifier si elles appartiennent au langage
engendré par la grammaire dont les productions sont données ci-après :
(1) S → AB
(2) A → aA
(3) A → a
(4) B → bB
(5) B → b
Pile Restant de la chaîne Action
(sommet à droite) à analyser
0 aabab# D4
0a4 abab# D4
0a4a4 bab# R "A → a"
0a4A5 bab# R "A → aA"
0A2 bab# D6
0A2b6 ab# "Echec"

Pile Restant de la chaîne Action


(sommet à droite) à analyser
0 aaabb# D4
0a4 aabb# D4
0a4a4 abb# D4
0a4a4a4 bb# R "A → a"
0a4a4A5 bb# R "A → aA"
0a4A5 bb# R "A → aA"
0A2 bb# D6
0A2b6 b# D6
0A2b6b6 # R "B → b"
0A2b6B7 # R "B → bB"
0A2B3 # R "S → AB"
0S1 # "Accepter"

95
5. Analyses syntaxiques ascendantes

V.4.1.6. Analyse SLR


L'analyse SLR pour Simple LR est appelée ainsi car c'est une analyse
"moins précise" que l'analyse LR. Par contre les tables d'analyses SLR sont,
en général, plus compactes que les tables d'analyses LR.
Contexte SLR(1) :
Soit G = <N,T,S,P> une grammaire non contextuelle ; on rajoute la règle
Z → S#k ; le couple (θ,a) est un contexte SLR(1) de la règle A → α de G
si et seulement si :
 Z ⇒+ βAω ⇒ βαω (dérivation droite) ;
 θ = βα est dit contexte gauche (contenu de la pile au cours de
l'analyse) ;
 a ∈ SUIVANT(A).
Contexte SLR(k) :
(θ,γ) est un contexte SLR(1) de la règle A → α de G si et seulement si :
 Z ⇒+ βAω ⇒ βαω (dérivation droite) ;
 θ = βα est dit contexte gauche (contenu de la pile au cours de
l'analyse) ;
 γ ∈ SUIVANTk(A).
Grammaires SLR(k) :
Soit G = <N,T,S,P> une grammaire non contextuelle, G est une
grammaire SLR(k) si et seulement si :
si θ est un contexte SLR(k) de la règle A → α de G
et θ.ω est un contexte SLR(k) de la règle B → β de G
avec θ ∈ (T ∪ N)*, ω ∈ (T ∪ {#})*, (A,B) ∈ (N)2; (α,β)∈ (T
∪ N)*
alors ω=ε, A=B et α=β
Remarque :
 L'analyse SLR est moins précise que l'analyse LR car après avoir
reconnu un contexte gauche (de la même manière dans les deux
analyses) l'analyseur SLR effectue une réduction par une règle A → α à
la "vue" de k symboles appartenant à SUIVANTk (A) alors que
l'analyseur LR effectue la réduction par A → α à la "vue" de k symboles
du contexte droit de la règle et on a : SUIVANTk (A) ⊇ k symboles du
contexte droit.
Théorème :
 Si une grammaire G est SLR(k) alors G est LR(k).
Exemple :
 La grammaire G dont les productions sont données ci-après est-elle
SLR(1) ?
96
5. Analyses syntaxiques ascendantes

E→E+T|T
T→T*F|F
F→i
√ La grammaire G précédente est SLR(1).
SUIVANT
E #+
T #+*
F #+*
Règle Contextes gauche Contextes SLR(1)
E→E+T E+T E+T#
E+T+
E→T T T#
T+
T→T*F E+T*F E+T*F#
E+T*F+
E+T*F*
T*F T*F#
T*F+
T*F*
T→F E+F E+F#
E+F+
E+F*
F F#
F+
F*
F→i i i#
i+
i*
E+i E+i#
E+i+
E+i*
T*i T*i#
T*i+
T*i*
E+T*i E+T*i#
E+T*i+
E+T*i*
Tableau V.5. Table des contextes SLR(1).

√ La grammaire G précédente est SLR(1).

97
5. Analyses syntaxiques ascendantes

Comment faire une analyse SLR ?


 Construire l'automate d'états finis qui reconnaît les contextes SLR d'une
grammaire ;
 Transcrire cet automate en table ;
 Si la table est mono-définie utiliser le même algorithme que pour
l'analyse LR.

V.4.2. Méthode pratique pour la construction d'analyseurs LR

V.4.2.1. Analyseurs SLR

V.4.2.1.1. Items LR(0)


Définition
 Un item LR(0) d'une grammaire G est une production avec un point
repérant une position du membre droit de cette production.
Exemples :
√ La production A → XYZ fournit quatre items que sont :
[A → .XYZ]
[A → X.YZ]
[A → XY.Z]
[A → XYZ.]
√ La production A → ε fournit l'item : [A → .]
Interprétation :
 Le point dans un item sépare le membre droit de production en partie
gauche (avant le point) et droite(après le point) ; cet item "signifie" qu'on
a reconnu une chaîne dérivée de la partie gauche et on attend de
reconnaître une chaîne dérivée de la partie droite.
Remarques :
 Un item peut être codé par deux entiers : le numéro de production et la
position du point dans le membre droit de cette production

V.4.2.1.2. Fermeture d'un ensemble d'iems


L'idée centrale d'un analyseur SLR est de construire à partir de la
grammaire un automate d'états finis AFD qui reconnaît les contextes
gauches d'une grammaire. Les items sont regroupés en ensembles qui
constitueront les états de l'AFD.
Remarque :
98
5. Analyses syntaxiques ascendantes

 La grammaire initiale sera augmentée de la règle S' → S (où S est


l'axiome de la grammaire) pour obtenir l'état initial de l'AFD.
Fermeture(I) :
 Soit I un ensemble d'items ;
 Placer chaque item de I dans Fermeture(I) ;
 Si [A → α.Bβ] est dans Fermeture(I) et B → γ est une production
Alors ajouter l'item [B → .γ] à Fermeture(I) s'il n'y est pas ;
 Appliquer cette règle jusqu'à ce qu'aucun item ne puisse être ajouté à
Fermeture(I).

Exemple :
 Considérer la grammaire G dont les productions sont données :
E→E+T|T
T→T*F|F
F → (E) | i
√ La grammaire est augmentée de la règle E' → E ;
√ Fermeture ([E' → .E]) = { [E' → .E], [E → .E+T], [E → .T],
[T → .T*F], [T → .F], [F → .(E)], [F → .i] }

V.4.2.1.3. Opération transition (ou GOTO)


 L'opération GOTO (I, X), où I est un état et X un symbole de la
grammaire, est définie comme suit :
GOTO(I, X) = Fermeture ([A →αX.β]) ;
tel que : [A → α.Xβ] ∈ I.

V.4.2.1.4. Construction de tous les ensembles d'items


L'algorithme suivant construit la collection canonique C, d'ensembles
d'items LR(0), pour une grammaire augmentée G' :

99
5. Analyses syntaxiques ascendantes

Procédure Items (G') ;


Début
C = {Fermeture([S' → S])} ;
Répéter
Pour chaque ensemble d'items I de C et
pour chaque symbole X de la grammaire
tel que GOTO(I,X) est non vide et non encore dans C
Faire ajouter GOTO(I,X) à C ;
Fait ;
Jusqu'à ce qu'aucun nouvel ensemble ne puisse être ajouté à C ;
Fin.

Exemple :
 Construire la collection canonique pour la grammaire augmentée G'
dont les productions sont données :
E' → E
E→E+T|T
T→T*F|F
F → (E) | i

√ I0 = {[E' → .E], [E → .E+T], [E → .T], [T → .T*F], [T → .F], [F →


.(E)], [F → .i]}
√ I1 = GOTO(I0, E) = {[E' → E.], [E → E.+T]}
√ I2 = GOTO(I0, T) = {[E → T.], [E → T.*F]}
√ I3 = GOTO(I0, F) = {[T → F.]}
√ I4 = GOTO(I0,() =
{[F→(.E)],[E→.E+T],[E→.T],[T→.T*F],[T→.F],[F→.(E)],[F→.i]}
√ I5 = GOTO(I0, i) = {[F → i.]}
√ I6 = GOTO(I1, +) = {[E → E+.T], [T → .T*F], [T → .F], [F → .(E)], [F
→.i]}
√ I7 = GOTO(I2, *) = {[T → T*.F], [F → .(E)], [F →.i]}
√ I8 = GOTO(I4, E) = {[E → E.+T], [F → (E.)]}
√ GOTO(I4, T) = I2
√ GOTO(I4, F) = I3
√ GOTO(I4, () = I4
√ GOTO(I4, i) = I5

100
5. Analyses syntaxiques ascendantes

√ I9 = GOTO(I6, T) = {[E → E+T.], [T → T.*F]}


√ GOTO(I6, F) = I3
√ GOTO(I6, () = I4
√ GOTO(I6, i) = I5
√ I10= GOTO(I7, F) = {[T → T*F.]}
√ GOTO(I7, () = I4
√ GOTO(I7, i) = I5
√ I11= GOTO(I8, )) = {[F → (E).]}
√ GOTO(I8, +) = I6
√ GOTO(I9, *) = I7

V.4.2.1.5. Algorithme de construction de la table SLR(1)


Donnée : Grammaire augmentée G' ;
Résultat : Table d'analyse SLR(1) pour G'.

i) Construire C = {I0, I1, …, In} la collection canonique d'items LR(0)


de G' ;
L'état i est construit à partir de Ii ;
ii) Les actions d'analyse pour l'état i sont obtenues par :
Si [A → α.aβ] est dans Ii, a ∈T et GOTO(Ii,a) = Ij
Alors ACTION[i, a] = "Décaler j" ;
FinSi ;
Si [A → α.] est dans Ii
Alors ACTION[i, b] = "Réduire par A → α" pour chaque b ∈
SUIVANT(A) ;
FinSi ;
Si [S' → S.] est dans Ii
Alors ACTION[i, #] = "Accepter";
FinSi ;
iii) Transition SUCCESSEUR pour l'état i
Si GOTO(Ii, A) = Ij ; A ∈N
Alors SUCCESSEUR [i, A] = j ;
iv) Toutes les entrées non définies par les règles ii) et iii) sont
positionnées à "Erreur"

101
5. Analyses syntaxiques ascendantes

Remarques :
 Si la table d'analyse ainsi construite est mono-définie alors la grammaire
est SLR(1). On pourra alors faire une analyse SLR(1).
 L'état initial de l'analyseur est celui construit à partir de l'ensemble
d'items contenant l'item [S' → .S].

V.4.2.1.6. Exemples
Exemple 1 :
 Construire la table d'analyse SLR(1) pour la grammaire augmentée G'
dont les productions numérotées sont données ci après :
E' → E
(1) E → E + T
(2) E → T
(3) T → T * F
(4) T → F
(5) F → (E)
(6) F → i
√ Calcul des ensembles SUIVANT :
SUIVANT
E +)#
T +)*#
F +)*#
√ La collection canonique étant déjà calculée, on obtient la table d'analyse
SLR(1) suivante :
+ * ( ) i # E T F
0 D4 D5 1 2 3
1 D6 Accepter
2 R (2) D 7 R (2) R (2)
3 R (4) R (4) R (4) R (4)
4 D4 D5 8 2 3
5 R (6) R (6) R (6) R (6)
6 D4 D5 9 3
7 D4 D5 10
8 D6 D 11
9 R (1) D 7 R (1) R (1)
10 R (3) R (3) R (3) R (3)
11 R (5) R (5) R (5) R (5)
Tableau V.6. Table d'analyse SLR(1).

102
5. Analyses syntaxiques ascendantes

√ La table est mono-définie donc la grammaire précédente est SLR(1) ;


analyser alors la chaîne suivante "i + i * i" :
Pile Restant de la Action
(sommet à droite) chaîne à analyser
0 i+i*i# D5
0i5 +i*i# R (6)
0F3 +i*i# R (4)
0T2 +i*i# R (2)
0E1 +i*i# D6
0E1+6 i*i# D5
0E1+6i5 *i# R (6)
0E1+6F3 *i# R (4)
0E1+6T9 *i# D7
0E1+6T9*7 i# D5
0E1+6T9*7i5 # R (6)
0 E 1 + 6 T 9 * 7 F 10 # R (3)
0E1+6T9 # R (1)
0E1 # Accepter
Exemple 2 :
 Construire la table d'analyse SLR(1) pour la grammaire augmentée G'
dont les productions numérotées sont données ci après (Grammaire des
pointeurs en C) :
S' → S
(1) S → L = R
(2) S → R
(3) L → * R
(4) L → a
(5) R → L
√ Calcul des ensembles SUIVANT :
SUIVANT
S #
L =#
R =#

103
5. Analyses syntaxiques ascendantes

√ Calcul de la collection canonique des ensembles d'items LR(0) :


- I0 = {[S' → .S], [S → .L=R], [S → .R], [L → .*R], [L → .a], [R → .L]}
- I1 = GOTO(I0, S) = {[S' → S.]}
- I2 = GOTO(I0, L) = {[S → L.=R], [R → L.]}
- I3 = GOTO(I0, R) = {[S → R.]}
- I4 = GOTO(I0, *) = {[L → *.R], [R → .L], {[L → .*R], [L → .a]}
- I5 = GOTO(I0, a) = {[L → a.]}
- I6 = GOTO(I2, =) = {[S → L=.R], [R → .L], {[L → .*R], [L → .a]}
- I7 = GOTO(I4, R) = {[L → *R.]}
- I8 = GOTO(I4, L) = {[R → L.]}
- GOTO(I4, *) = I4
- GOTO(I4, a) = I5
- I9 = GOTO(I6, R) = {[S → L=R.]}
- GOTO(I6, L) = I8
- GOTO(I6, *) = I4
- GOTO(I6, a) = I5
√ Table d'analyse :
= * a # S L R
0 D4 D5 1 2 3
1 Accepter
2 D6 R (5)
/R(5)
3 R (2)
4 D4 D5 8 7
5 R (4) R (4) 8 9
6 D4 D5
7 R (3) R (3)
8 R (5) R (5)
9 R (1)
√ La table d'analyse a une multidéfinition donc la grammaire n'est pas
SLR(1).

104
5. Analyses syntaxiques ascendantes

V.4.2.2. Analyse LR(1)


L'analyse SLR(1) étant restreinte (classe des grammaires SLR(1) n'est pas
très grande), il faut disposer donc d'un analyseur plus "large" (acceptant plus
de grammaires). L'analyse LR(1) permet d'éviter les réductions invalides en
attachant plus d'information à un état. Les items vont inclure cette
information qui en fait le premier symbole du contexte droit d'une règle.

V.4.2.2.1. Items LR(1)


Représentation :
 Un item LR(1) sera représenté sous la forme suivante :

[A → α.β
β, a]
où A → αβ est une production et a le symbole de prévision.
Remarques :
 Un item LR(1) de la forme [A → α, a] implique de réduire par la
production A→α uniquement lorsque le prochain symbole d'entrée est a.
 Avec un item LR(1) de la forme [A → α.β β, a] avec β≠ε, la prévision
n'a aucun effet.
 Dans un item [A → α.β β, a], l'ensemble des symboles de prévision ⊆
SUIVANT(A).

V.4.2.2.2. Opération Fermeture


L'opération fermeture est quelque peu modifiée par rapport à celle
définie pour les items LR(0).
Fermeture(I) :
 Soit I un ensemble d'items LR(1) ;
 Placer chaque item de I dans Fermeture(I) ;
 Si [A → α.Bβ, a] est dans Fermeture(I) et B → γ est une production
Alors ajouter l'item [B → .γ, b] à Fermeture(I) avec b ∈ DEBUT(βa) s'il n'y
est pas ;
 Appliquer cette règle jusqu'à ce qu'aucun item ne puisse être ajouté à
Fermeture(I).

V.4.2.2.3. Opération GOTO


 L'opération GOTO (I, X), où I est un état et X un symbole de la
grammaire, est définie comme suit :
GOTO(I, X) = Fermeture ([A →αX.β, a]) ;
tel que : [A → α.Xβ, a] ∈ I.

105
5. Analyses syntaxiques ascendantes

V.4.2.2.4. Construction de tous les ensembles d'items LR(1)


L'algorithme suivant construit la collection canonique C, d'ensembles
d'items LR(1), pour une grammaire augmentée G' :
Procédure Items (G') ;
Début
C = {Fermeture([S' → S, #])} ;
Répéter
Pour chaque ensemble d'items I de C et
pour chaque symbole X de la grammaire
tel que GOTO(I,X) est non vide et non encore dans C
Faire ajouter GOTO(I,X) à C ;
Fait ;
Jusqu'à ce qu'aucun nouvel ensemble ne puisse être ajouté à C ;
Fin.

V.4.2.2.5. Algorithme de construction de la table LR(1)


Donnée : Grammaire augmentée G' ;
Résultat : Table d'analyse LR(1) pour G'.
Algorithme :
i) Construire C = {I0, I1, …, In} la collection canonique d'items LR(1)
de G' ;
L'état i est construit à partir de Ii ;
ii) Les actions d'analyse pour l'état i sont obtenues par :
Si [A → α.aβ, b] est dans Ii, (a,b) ∈T2 et GOTO(Ii,a) = Ij
Alors ACTION[i, a] = "Décaler j" ;
FinSi ;
Si [A → α., a] est dans Ii
Alors ACTION[i, a] = "Réduire par A → α" ;
FinSi ;
Si [S' → S., #] est dans Ii
Alors ACTION[i, #] = "Accepter";
FinSi ;
iii) Transition SUCCESSEUR pour l'état i
Si GOTO(Ii, A) = Ij ; A ∈N
Alors SUCCESSEUR [i, A] = j ;
iv) Toutes les entrées non définies par les règles ii) et iii) sont
positionnées à "Erreur"
106
5. Analyses syntaxiques ascendantes

Remarques :
 Si la table d'analyse ainsi construite est mono-définie alors la grammaire
est LR(1). On pourra alors faire une analyse LR(1).
 L'état initial de l'analyseur est celui construit à partir de l'ensemble
d'items contenant l'item [S' → .S, #].

Exemple
V.4.2.2.6.
 Construire la table d'analyse LR(1) pour la grammaire augmentée G'
dont les productions numérotées sont données ci après :
S' → S
(1) S → AA
(2) A → aA
(3) A → b
√ Calcul de la collection canonique des ensembles d'items LR(1) :
- I0 = {[S' → .S, #], [S → .AA, #], [A → .aA, a/b], [A → .b, a/b]}
- I1 = GOTO(I0, S) = {[S' → S., #]}
- I2 = GOTO(I0, A) = {[S → A.A, #], [A → .aA, #], [A → .b, #]}
- I3 = GOTO(I0, a) = {[A → a.A, a/b], [A → .aA, a/b], [A → .b, a/b]}
- I4 = GOTO(I0, b) = {[A → b., a/b]}
- I5 = GOTO(I2, A) = { [S → AA., #]}
- I6 = GOTO(I2, a) = {[A → a.A, #], [A → .aA, #], [A → .b, #]}
- I7 = GOTO(I2, b) = {[A → b., #]}
- I8 = GOTO(I3, A) = {[A → aA., a/b]}
- GOTO(I3, a) = I3
- GOTO(I3, b) = I4
- I9 = GOTO(I6, A) = {[A → aA., #]}
- GOTO(I6, a) = I6
- GOTO(I6, b) = I7
√ Table d'analyse LR(1):
a b # S A
0 D3 D4 1 2
1 Accepter
2 D6 D7 5
3 D3 D4 8
4 R (3) R (3)
5 R (1)
6 D6 D7 9
7 R (3)
8 R (2) R (2)
9 R (2)
107
5. Analyses syntaxiques ascendantes

√ La table d'analyse est mono-définie donc la grammaire est LR(1).

V.4.2.3. Analyse LALR(1)


L'analyse LALR pour Look Ahead LR est un très bon compromis
entre les analyses SLR et LR. Le générateur de compilateur YACC utilise la
méthode LALR(1) pour la partie syntaxique du compilateur.
La classe des grammaires pouvant être analysées par une méthode LR
est beaucoup grande que la classe des grammaires pouvant être analysées
par une méthode SLR, mais les tables SLR sont en général plus compactes
que les tables LR. Pour un langage de programmation les tables LR peuvent
avoisiner les milliers d'états alors que les tables SLR sont de l'ordre de
centaines d'états.
Un analyseur LALR utilise des tables qui ont strictement le même
nombre d'états qu'un analyseur SLR. Mais la classe des grammaires LALR
est plus grande que la classe des grammaires SLR. La classe des grammaires
LALR est par contre moins grande que la classe des grammaires LR. La
relation entre ensembles de grammaires est illustré par le schéma suivant :
LR(k)

LALR(k)

SLR(k)

V.4.2.3.1. Méthode 1 de construction des tables LALR(1)


Le point de départ de cette méthode est la collection canonique d'items
LR(1). L'idée est de regrouper les ensembles d'items qui ont un même
"cœur" i.e. les ensembles qui ont exactement les mêmes parties gauche des
items LR(1). Les symboles de prévision sont différents. Les tables d'analyses
seront considérablement réduites.
Exemple :
 Les états I3 et I6 de la grammaire précédente ont même cœur ; ils vont
être regroupés en un seul état I36.
Algorithme :
L'algorithme suivant donne la table d'analyse LALR(1) à partir des items
LR(1). C'est un algorithme à but pédagogique (pour comprendre le
108
5. Analyses syntaxiques ascendantes

principe). Ce n'est pas cet algorithme qui sera implémenté pour obtenir les
tables LALR.
i) Construire C = {I0, I1, …, In} la collection canonique d'items LR(1) ;
ii) Rechercher les ensembles d'items ayant même cœur ; Les fusionner en
un seul état ;
iii) Soit C' = {J0, J1, …, Jm} la nouvelle collection d'items ;
L'opération GOTO est obtenue de la façon suivante :
Si J = I0 ∪ I1 ∪ … ∪ Is
Alors K = ∪ GOTO(Ii,X) , i=1..s ;
GOTO (J,X) = K ;
FinSi ;
iv) Les réductions sont obtenues en examinant C'.

Application :
 Construire la table d'analyse LALR(1) pour la grammaire LR(1) de la
section 5.2.1.6.
√ Regroupement des états ayant même cœur :
- Les états I3 et I6 donnent l'état I36= {[A → a.A, a/b/#], [A → .aA,
a/b/#], [A → .b, a/b/#]}
- Les états I4 et I7 donnent l'état I47= {[A → b., a/b/#]}
- Les états I8 et I9 donnent l'état I89= {[A → aA., a/b/#]}
√ Table d'analyse LALR(1) :
a b # S A
"0" D "36" D "47" "1" "2"
"1" Accepter
"2" D "36" D "47" "5"
"36" D "36" D "47" "89"
"47" R (3) R (3)
"5" R (1)
"89" R (2) R (2)
Théorème :
 Si la table d'analyse LALR(1) d'une grammaire G est mono-définie alors
G est LALR(1).
Remarques :
i) Un analyseur LR détecte une erreur le plus tôt au cours de l’analyse.

109
5. Analyses syntaxiques ascendantes

ii) Un analyseur LALR ou SLR peut faire des réductions supplémentaires


avant de détecter l'erreur mais jamais de décalage de symbole erroné.
iii) Le nombre de décalage est le même pour les trois types d'analyse.
Exemple :
 Analyser la chaîne aab# pour vérifier sont appartenance au langage
engendré par la grammaire de la section 5.2.1.6 en utilisant les méthodes
LR(1) puis LALR(1).
√ Analyse LR(1) :
Pile Restant de la chaîne Action
(sommet à droite) à analyser
0 aab# D3
0a3 ab# D3
0a3a3 b# D4
0a3a3b4 # "Erreur"
√ Analyse LALR(1) :
Pile Restant de la chaîne Action
(sommet à droite) à analyser
0 aab# D 36
0 a 36 ab# D 36
0 a 36 a 36 b# D 47
0 a 36 a 36 b 47 # R (3)
0 a 36 a 36 A 89 # R (2)
0 a 36 A 89 # R (2)
0A2 # "Erreur"

V.4.2.3.2. Méthode 2 de construction des tables LALR(1)


Le point de départ de cette méthode est la collection canonique d'items
LR(0). L'idée consiste à rajouter aux items LR(0) une information
supplémentaire (le symbole de prévision). Il est à noter qu'il ne s'agit pas
d'items LR(1). Cette méthode est celle qui sera implémentée car plus
efficace.
Remarque :
 Les tables obtenues par les deux méthodes sont exactement les mêmes.
Algorithme :
L'algorithme suivant donne la table d'analyse LALR(1) à partir des items
LR(0).

110
5. Analyses syntaxiques ascendantes

i) Construire C = {I0, I1, …, In} la collection canonique d'items LR(0) ;


ii) Pour chaque ensemble item, garder uniquement son noyau ; un
noyau d'un ensemble d'items LR(0) est formé des items qui n'ont pas le
"point" (".") en début de membre droit de production ; toutefois le
premier noyau devra comprendre l'item [S' →.S] ;
iii) Détermination des symboles de prévision :
Commencer par l'item [S' →.S] de I0 ;
J = Fermeture ([S' →.S, #] ;
Si [A → α.Xβ, a] est dans J
Alors le symbole de prévision a est engendré pour l'item
[A → αX.β] dans GOTO(J,X) ;
l'item deviendra [A → αX.β, a] ;
FinSi ;
Fermeture des items avec symboles de prévision des autres noyaux ;
iv) Refaire l'étape iii) jusqu'à ce qu'aucun symbole de prévision ne
puisse être ajouté.
Application :
 Construire la table d'analyse LALR(1) pour la grammaire G suivante :
S' → S
(1) S → aA
(2) S → bAb
(3) S → ab
(4) A → ε
√ Calcul de la collection canonique des ensembles d'items LR(0) :
- I0 = {[S' → .S], [S → .aA], [S → .bAb], [S → .ab]}
- I1 = GOTO(I0, S) = {[S' → S.]}
- I2 = GOTO(I0, a) = {[S → a.A], [S → a.b], [A → .]}
- I3 = GOTO(I0, b) = {[S → b.Ab], [A → .]}
- I4 = GOTO(I2, A) = {[S → aA.]}
- I5 = GOTO(I2, b) = {[S → ab.]}
- I6 = GOTO(I3, A) = {[S → bA.b]}
- I7 = GOTO(I6, b) = {[S → bAb.]}
√ Ensembles SUIVANT :
SUIVANT
S #
A b#
111
5. Analyses syntaxiques ascendantes

√ Table d'analyse SLR(1) :


a b # S A
0 D2 D3 1
1 Accepter
2 D5 / R(4) R(4) 4
3 R(4) R(4) 6
4 R(1)
5 R(3)
6 D7
7 R(2)
√ La grammaire G n'étant pas SLR, calcul des noyaux d'items :
- I0 = {[S' → .S]}
- I1 = {[S' → S.]}
- I2 = {[S → a.A], [S → a.b]}
- I3 = {[S → b.Ab]}
- I4 = {[S → aA.]}
- I5 = {[S → ab.]}
- I6 = {[S → bA.b]}
- I7 = {[S → bAb.]}
√ Propagation des symboles de prévision sur ces noyaux d'items :
- I0 = {[S' → .S,#], [S → .aA,#], [S → .bAb,#], [S → .ab,#]}
- I1 = GOTO(I0, S) = {[S' → S.,#]}
- I2 = GOTO(I0, a) = {[S → a.A,#], [S → a.b,#], [A → .,#]}
- I3 = GOTO(I0, b) = {[S → b.Ab,#], [A → .,b]}
- I4 = GOTO(I2, A) = {[S → aA.,#]}
- I5 = GOTO(I2, b) = {[S → ab.,#]}
- I6 = GOTO(I3, A) = {[S → bA.b,#]}
- I7 = GOTO(I6, b) = {[S → bAb.,#]}

112
5. Analyses syntaxiques ascendantes

√ Table d'analyse LALR(1) :


a b # S A
0 D2 D3 1
1 Accepter
2 D5 R(4) 4
3 R(4) 6
4 R(1)
5 R(3)
6 D7
7 R(2)
√ La table est mon-définie donc la grammaire G est LALR(1).
Application 2 :
 Construire la table d'analyse LALR(1) pour la grammaire G suivante :
S' → S
(1) S → L = R
(2) S → R
(3) L → * R
(4) L → a
(5) R → L
√ Cette grammaire n'est pas SLR(1) (voir section 5.2.1.6).
√ Propagation des symboles de prévision sur les noyaux d'items LR(0):
- I0 = {[S' → .S,#], [S → .L=R,#], [S → .R,#], [L → .*R,=/#], [L →
.i,=/#], [R → .L,#]}
- I1 = {[S' → S.,#]}
- I2 = {[S → L.=R,#], [R → L.,#]}
- I3 = {[S → R.,#]}
- I4 = {[L → *.R,=/#], [R → .L,=/#], [L → .*R,=/#], [L → .i,=/#]}
- I5 = {[L → i.,=/#]}
- I6 = {[S → L=.R,#], [R → .L,#], [L → .*R, #], [L → .i,#]}
- I7 = {[L → *R., =/#]]}
- I8 = {[R → L.,=/#]}
- I9 = {[S → L=R.,#]}

113
5. Analyses syntaxiques ascendantes

√ Table d'analyse LALR(1) :

* = i # S L R

0 D4 D5 1 2 3

1 Accept
er

2 D6 R(5)

3 R(2)

4 D4 D5 8 7

5 R(4) R(4)

6 D4 D5 8 9

7 R(3) R(3)

8 R(5) R(5)

9 R(1)

√ La table est mon-définie donc la grammaire G est LALR(1).

V.4.3. Utilisation des grammaires ambiguës


Remarques :
 Une grammaire ambiguë n'est pas LR(k) ∀k≥0, alors pourquoi utiliser
des grammaires ambiguës ?
 Pour exploiter les grammaires ambiguës dans une analyse LR, on
construit les tables d'analyses LR qui sont forcément multi-définies puis
on lève ces multidéfinitions par des règles comme par exemple les règles
de priorité et associativité. Les avantages de procéder ainsi sont de
réduire les tailles des tables d'analyse et d'accélérer les analyses.
Exemple :
 Construire la table d'analyse SLR(1) pour la grammaire ambiguë
suivante :
E → E + E | E * E | (E) | i

114
5. Analyses syntaxiques ascendantes

 Items LR(0) :
√ I0 = {[E' → .E], [E → .E+E], [E → .E*E], [E → .(E)], [E → .i]}
√ I1 = GOTO(I0, E) = {[E' → E.], [E → E.+E], [E → E.*E]}
√ I2 = GOTO(I0,() = {[E→(.E)], [E→.E+E], [E → .E*E], [E → .(E)], [E
→ .i]}
√ I3 = GOTO(I0, i) = {[E → i.]}
√ I4 = GOTO(I1, +) = {[E → E+.E], [E→.E+E], [E → .E*E], [E → .(E)],
[E → .i]}
√ I5 = GOTO(I1, *) = {[E → E*.E], [E→.E+E], [E → .E*E], [E → .(E)],
[E → .i]}
√ I6 = GOTO(I2, E) = {[E → (E.)], [E → E.*E], [E→E.+E]}
√ GOTO(I2, () = I2
√ GOTO(I2, i) = I3
√ I7 = GOTO(I4, E) = {[E → E+E.], [E → E.*E], [E→E.+E]}
√ GOTO(I4, () = I2
√ GOTO(I4, i) = I3
√ I8 = GOTO(I5, E) = {[E → E*E.], [E → E.*E], [E→E.+E]}
√ GOTO(I5, () = I2
√ GOTO(I5, i) = I3
√ I9 = GOTO(I6, )) = {[E → (E).]}
√ GOTO(I6, +) = I4
√ GOTO(I6, *) = I5
√ GOTO(I7, +) = I4
√ GOTO(I7, *) = I5
√ GOTO(I8, +) = I4
√ GOTO(I8, *) = I5
 Calcul de l'ensemble SUIVANT :
SUIVANT
E *+)#

115
5. Analyses syntaxiques ascendantes

 Table d'analyse SLR(1) :

i + * ( ) # E

0 D3 D2 1

1 D4 D5 Accepter

2 D3 D2 6

3 R(4) R(4) R(4) R(4)

4 D3 D2 7

5 D3 D2 8

6 D4 D5 D9

7 D4 D5 R(1) R(1)
R(1) R(1)

8 D4 D5 R(2) R(2)
R(2) R(2)

9 R(3) R(3) R(3) R(3)

 Les multi-définitions sont levées en utilisant les priorités et


associativités des opérateurs. On garde dans les "cases" multi-définies
les actions suivantes :

+ *

7 R(1) D5

8 R(2) R(2)

V.4.4. Gestion des erreurs en analyse LR

V.4.4.1. Récupération sur erreur en mode panique


 La récupération sur erreur peut se faire de la manière suivante :

116
5. Analyses syntaxiques ascendantes

o Dépiler jusqu'à trouver un non-terminal A ;


o Avancer dans la chaîne à analyser jusqu'à trouver un SUIVANT
de A ;
o Empiler SUCCESSEUR (S, A) où S est l'état en dessous du
sommet de pile A ;
o Reprendre l'analyse.

V.4.4.2. Routines d'erreurs


 Les routines de signalement d'erreurs sont à insérer dans les cases vides
de la table d'analyse.
 Les messages d'erreurs doivent être significatifs autant que possible.
Exemple :
 Routines d'erreurs dans la table précédente :
i + * ( ) # E
0 D3 ER1 ER1 D2 ER2 ER1 1
1 ER3 D4 D5 ER3 ER2 Accepter

2 D3 ER1 ER1 D2 ER2 ER1 6


3 R(4) R(4) R(4) R(4) R(4) R(4)
4 D3 ER1 ER1 D2 ER2 ER1 7
5 D3 ER1 ER1 D2 ER2 ER1 8
6 ER3 D4 D5 ER3 D9 ER4
7 R(1) D4 D5 R(1) R(1) R(1)
R(1) R(1)
8 R(2) D4 D5 R(2) R(2) R(2)
R(2) R(2)
9 R(3) R(3) R(3) R(3) R(3) R(3)
 ER1 : Opérande manquant ;
 ER2 : Parenthèse fermante en plus ;
 ER3 : Opérateur manquant.

V.4.5. Classification des grammaires


 Pour affirmer qu’une grammaire est non ambiguë, on dispose de
conditions suffisantes mais non nécessaires.

117
5. Analyses syntaxiques ascendantes

Théorème :
 Si une grammaire G est LL, de précédence simple, de précédence fiable
ou LR (SLR,LALR ou LR) alors G est non ambiguë

Remarques :
 La classe des grammaires qui peuvent être analysées par la méthode LR
est un sur-ensemble strict de la classe des grammaires qui peuvent être
analysées par les méthodes prédictives.
 Une grammaire non ambiguë peut ne pas être LR(k) ∀ k ≥ 0. La
grammaire des palindromes dont les productions sont S→aSa|ε est non
ambiguë et non LR(k) ∀ k ≥ 0.

Grammaires non ambiguës


LR(1)

LALR(1)

SLR(1) LL(1)

Figure V.4. Classification de grammaires

118
5. Analyses syntaxiques ascendantes

V.5. EXERCICES

Exercice 1.
 Soit G la grammaire dont les productions sont :
S → (L) | a
L→L,S|S
a) G est-elle d'opérateurs ? G est-elle de précédence d'opérateurs?
b) Analyser la chaîne : (a , (a , a))

Exercice 2.
 Soit G la grammaire dont les productions sont :
S→{S,A}|A
A→A(-A)|a|b
a) G est-elle de précédence simple ?
b) Comment rendre la grammaire précédente de précédence simple ?

Exercice 3.
 Soit G une grammaire régulière. L(G) peut-il être engendré par une
grammaire :
a) de précédence simple ?
b) de précédence d'opérateurs ?

Exercice 4.
 Soit la grammaire G=({S,A},{`,´,c},P,S).
P: S → A´
A → ` | Ac | AS
a) G est-elle de précédence simple ?
b) Analyser la chaîne `c`c´´
c) Construire le graphe d’optimisation de G.
d) Analyser la chaîne `c`cc´´ avec la table optimisée.

119
5. Analyses syntaxiques ascendantes

Exercice 5.
• On définit la grammaire G=({S,A},{a,b,c,[,]},P,S).
P: S → AS | a S | [S] | t | f
A→S b |S c
a) Montrer que G n'est pas de précédence simple sans calculer les relations
de précédence.
b) Construire une grammaire équivalente G’ qui donne aux opérateurs a b c
les priorités et les associativités suivantes :
 a est plus prioritaire que b
 b est plus prioritaire que c
 b et c sont associatifs à gauche
 Les expressions sont évaluées en premier
 L’évaluation se fait de gauche à droite
 L’expression la plus simple a pour valeur f ou t
a) La grammaire G’ est-elle de précédence d’opérateurs ?

Exercice 6.
• Soit la grammaire G=({S,A,B},{0,1,a},P,S).
P: S → 0A1
A → aSB | 00
B→1
a) G est-elle de précédence simple ? est-elle de précédence faible ?
b) Construire le graphe d’optimisation de la table de précédence.
c) Construire la table de précédence optimisée.

Exercice 7.
• Soit la grammaire G=({S,A},{a,b,d},P,S).
P: S → bAdS | aa
A → aS | a | d
a) G est-elle de précédence simple ?
b) G est-elle de précédence faible ?
c) Construire le graphe d’optimisation de G.
d) Analyser la chaîne badaa

120
5. Analyses syntaxiques ascendantes

Exercice 8.
• On définit la grammaire G=(N,T,P,S) suivante :
P: S → aAB|d
A → bSB|a
B → b
a) Est-ce que G est une grammaire de précédence simple ? Si elle ne l'est
pas qu'elle modification permettrait de faire une analyse déterministe.

Exercice 9.
• Soit la grammaire G=({S,A,B},{a,b},P,S).
P: S → aSAB | BA
A → aA | B
B→b
a) G est-elle LR(1) ?
b) Analyser la chaîne abbbba.

Exercice 10.
• Considérer les grammaires suivantes :

G1: S → abAB | bAB G2: S → Sbc | a


A → aA | ε
B → bA | ε
G3: S → abA | ε G4: S → aA | a
A → Saa | b A → aS | a
• G1, G2, G3 et G4 sont-elles LR(k) ?

Exercice 11.
• Soit la grammaire Gm,n=({S,A,B},{a,b,c},P,S) ; m,n >= 0
P: S → aA | aS
A → bn | Ban
B → cB | bm
• Gm,n est-elle LR(k) ? (Discuter).

121
5. Analyses syntaxiques ascendantes

Exercice 12.
• Soit la grammaire G=({S,A,B},{a,b},P,S).
P: S → AaAb | BbBa
A→ε
B→ε
a) G est-elle LL(1) ?
b) G est-elle SLR(1) ?

Exercice 13.
• Soit la grammaire G=({S,A},{a,b,c},P,S).
P: S → AA | cAc
A → aA | b
a) G est-elle LR(1) ?
b) G est-elle LALR(1) ?

Exercice 14.
• Soit G une grammaire LR(0).
a) Peut-on dire que G est SLR(0) ? LALR(0) ?
b) Peut-on dire que G est LL(1) ? LL(k) ? (k>1).

Exercice 15.
• Considérer la grammaire G suivante :
P: E→E+T|T
T→TF|F
F→F*|a|b
• Construire la table d’analyse SLR(1) pour cette grammaire.
• Construire la table d’analyse LALR(1)

122
5. Analyses syntaxiques ascendantes

Exercice 16.
 On définit la grammaire G=(N,T,P,S) suivante :
P: <instr> → si cond alors <instr> sinon <instr>
| si cond alors <instr>
| autre
 si, cond, alors, sinon et autre sont considérés comme des terminaux.
a) Réduire l'écriture de cette grammaire.
b) Cette grammaire est-elle SLR(1) ?
c) S'il existe des conflits, utiliser la convention du langage Pascal pour
supprimer ce (ou ces) conflits.

Exercice 17.
• Considérer la grammaire G suivante :
P: S → Aa | bAc | dc | bda
A→d
• Montrer que cette grammaire est LALR(1) mais pas SLR(1).

Exercice 18.
• Considérer la grammaire G suivante :
P: S → Aa | bAc | Bc | bBa
A→d
B→d
• Montrer que cette grammaire est LR(1) mais pas LALR(1).

Exercice 19.
• On définit la grammaire Gi,j=({S,A,B},{a,b,c},P,S). i, j entiers positifs
fixés.
P: S → aA | aS
A → (ab)i | Bab
B → Bc | (ab)j
a) La grammaire Gi,j est-elle SLR(0) ?
b) La grammaire G0,0 est-elle LR(1) ?
c) La grammaire Gi,j est-elle LR(k) ?

123
5. Analyses syntaxiques ascendantes

Exercice 20.
• Soit la grammaire G=({E},{⊗,⊕,(,),i},P,E) :
P: E → E ⊕ E | E ⊗ E | (E) | i
a) Construire la table d'analyse SLR(1) pour la grammaire précédente.

Exercice 21.
a) Donnez la grammaire ambiguë qui permet de générer les expressions
régulières définies sur l'alphabet {a,b}. Les symboles |, ., * et ∈
représentent respectivement l'union, la concaténation, la fermeture et
épsilon.
b) Construire la table d'analyse LALR(1) pour la grammaire trouvée en a)
en utilisant la méthode optimisée.
c) Lever les ambiguités de votre table en utilisant les priorités et
associativités liées aux opérateurs de la grammaire.
d) Analyser la chaîne : a|b.a*

124
6. Traduction Dirigée par la Syntaxe

CHAPITRE VI. TRADUCTION DIRIGEE


PAR LA SYNTAXE

Introduction
Dans ce chapitre, on traitera de l'aspect traduction dirigée par la syntaxe
c'est à dire qu'au fur à mesure que se déroule l'analyse syntaxique, on
effectue certains traitements. Ces traitements sont appelés des actions
sémantiques ou des routines sémantiques. Ces actions ont un "timing"
d'exécution. Les traitements à effectuer sont insérés directement dans la
grammaire. Le fait de procéder ainsi, permet d'éviter de faire des passes
répétées sur le programme à analyser.
Tout traitement nécessaire à la production d'un résultat (production de
code intermédiaire, contrôles sémantiques, …) est inséré dans la
grammaire à "un endroit adéquat". Les sections suivantes détailleront ce
principe.

VI.1. LANGAGES INTERMEDIAIRES

 Cette section est consacrée aux différentes formes de code intermédiaire


qu'on peut générer après analyse du programme source.

VI.1.1. Notation post-fixée

Notation post-fixée pour une expression E :


 Si E est une constante ou une variable, notation de E est E
 Si E est une expression de la forme E1 op E2
où op est un opérateur binaire
Alors la notation de E et E1' E2' op
où E1' E2' sont les notations post-fixée de E1 et E2
 La notation post-fixée de (E) est la notation post-fixée de E.

125
6. Traduction Dirigée par la Syntaxe

Remarques :
i) La notation post-fixée est surtout utilisée pour représenter les
expressions arithmétiques. L'évaluation d'une expression arithmétique
devient "très facile".
ii) Aucune parenthèse n'est nécessaire en notation post-fixée.
Exemple :
• Représentation en notation post-fixée de : a := b * (a - c) * a
bac-*a*

VI.1.2. Qudruplets

Définition :
 Un quadruplet est une structure à quatre champs :
(op, source1, source2, destination)
qui désigne : destination ← source1 op source2
Remarques :
• La représentation du code intermédiaire sous forme de quadruplets est
très utilisée dans la compilation des langages de programmation "haut
niveau". Car cette forme de représentation se rapproche du code
machine.
• Les instructions à opérateur unaire sont représentés par :
(op, source1, , destination)
• Les instructions de branchement sont représentés par :
(code-br, , , label)

Exemple :
• Représentation sous forme de quadruplets de l'expression : a := b * (- c)
+ b * (- c)
(0) (- , c , , T1 )
(1) (*, b , T1 , T2 )
(2) (- , c , , T3 )
(3) (*, b , T3 , T4 )
(4) (+, T2 , T4 , T5 )
(5) (=, T5 , , a )

126
6. Traduction Dirigée par la Syntaxe

VI.1.3. Triplets et Triplets indirects

VI.1.3.1. Triplets
Définition :
 Un triplet est une structure à trois champs :
(op, Arg1, Arg2)
qui effectue : Arg1 op Arg2 ; le résultat de cette opération est référencé
par le numéro du triplet dans le code intermédiaire généré. Les
arguments peuvent également un triplet.
Exemple :
• Représentation sous forme de triplets de l'expression : a := b * (- c) + b *
(- c)
(0) (- ,c , )
(1) (*, b , (0) )
(2) (- ,c , )
(3) (*,b , (2) )
(4) (+,(1), (3) )
(5) (=, a , (4) )

VI.1.3.2. Triplets indirects


Définition :
 La représentation sous format de triplets indirects est une liste de
référence vers des triplets.
Remarque :
i) Cette représentation évite les redondances qu'on peut avoir dans la
représentation sous forme de triplets.
ii) Elle permet également de facilement réordonner le code
intermédiaire ou de supprimer "facilement" certaines instructions
inutiles.
Exemple :
• Représentation sous forme de triplets de l'expression : a := b * (- c) + b *
(- c)

127
6. Traduction Dirigée par la Syntaxe

• Code intermédiaire :
(0)
(1)
(0)
(1)
(2)
(3)
• Triplets :
(0) (- ,c , )
(1) (*, b , (0) )
(2) (+,(1), (1) )
(3) (=, a , (4) )

VI.1.4. Arbres abstraits

Définition :
 La représentation sous format d'arbres abstraits est une forme condensée
de l'arbre syntaxique.
Exemple :
• Représentation sous forme de triplets de l'expression : 3 * 5 + 4

* 4

3 5

VI.2. DEFINITIONS DIRIGEES PAR LA SYNTAXE

VI.2.1. Attributs des symboles de la grammaire

 Une définition dirigée par la syntaxe est une généralisation d'une


grammaire non contextuelle, dans laquelle chaque symbole de l'arbre
128
6. Traduction Dirigée par la Syntaxe

syntaxique (symboles de la grammaire) possède un ensemble d'attributs.


Les attributs d'un symbole sont champs d'une structure (valeur, type,
…).
 Les attributs d'un symbole peuvent être stockés dans la pile utilisée pour
l'analyse syntaxique ou dans une pile "parallèle" à la pile "syntaxique".
 Les attributs peuvent être classés en deux catégories que sont :
√ Les attributs hérités ;
√ Les attributs synthétisés
Attribut synthétisé :
 Une valeur synthétisée d'un symbole de la grammaire est calculée à
partir des descendants de ce symbole dans l'arbre syntaxique.
Attribut hérité :
 Une valeur héritée d'un symbole de la grammaire est calculée à partir des
ascendants et frères de ce symbole dans l'arbre syntaxique.
Arbre décoré :
 On appelle arbre syntaxique décoré (ou arbre annoté), un arbre
syntaxique muni des valeurs d'attributs en chaque sommet de l'arbre.

VI.2.2. Définitions n'utilisant que des attributs synthétisés


 Ce type de définition facilite la traduction d'un programme source car le
passage d'informations entre sommets d'un arbre syntaxique est simple.
 On notera X.val l'attribut associé à un symbole X de la grammaire.
Exemple :
 La définition suivante n'utilise que des attributs synthétisés pour
l'évaluation d'une expression arithmétique :

Production Règle sémantique


L → E '\n' Imprimer (E.val)
E→E +T E.val := E.val + T.val
E→T E.val := T.val
T→T*F T.val := T.val * F.val
T→F T.val := F.val
F→(E) F.val := E.val
F → chiffre F.val := Chiffre.val
129
6. Traduction Dirigée par la Syntaxe

 Arbre décoré pour l'expression suivante : 3 * 5 + 4

E.val=19 '\n'

E.val=15 + T.val=4

T.val=15 F.val=4

T.val=3 F.val=15 4
*

F.val=3 5

VI.2.3. Définitions utilisant des attributs hérités


 Les attributs hérités sont utilisés dans la déclaration des variables dans
plusieurs langages de programmation (comme le langage C par
exemple).
Exemple :
 Définition de déclaration de variables :

Production Règle sémantique


D→TL L.typeh ← T.type
T → 'entier' T.type ← entier
T → 'réel' T.type ← réel
L → L1 , id L1.typeh ← L.typeh
L → id Ajouter id à la table des symboles

 Arbre décoré pour l'expression suivante : réel id1, id2, id3

130
6. Traduction Dirigée par la Syntaxe

T.type = réel L.typeh = réel

réel L.typeh = réel , id3

L.typeh = réel , id2

id1

VI.3. SCHEMAS DE TRADUCTION

VI.3.1. Définition

 Un schéma de traduction est une grammaire context-free dans laquelle


des attributs sont associés aux symboles de la grammaire et des actions
sémantiques (code en C ou autre) sont insérées dans les MDP.

VI.3.2. Conception d'un schéma de traduction


 La conception d'un schéma de traduction peut être résumée par les étapes
suivantes :
i) Trouver la bonne grammaire
ii) Assurer que la valeur d'un attribut est disponible quand une action
s'y réfère.
iii) Insérer les routines sémantiques aux bons attributs (en fonction du
problème à résoudre)
Règles à respecter :
 Un attribut hérité d'un symbole dans un MDP (Membre Droit de
Production) doit être calculé dans une action située avant ce symbole
 Une action ne doit pas faire référence à un attribut synthétisé d'un
symbole situé à droite de l'action
 Un attribut synthétisé du MGP (Membre Gauche de Production) est
calculé après tous les attributs dont il dépend
131
6. Traduction Dirigée par la Syntaxe

VI.3.3. Méthodologie de génération de code intermédiaire

 Suivre les étapes suivantes pour définir un traducteur dirigé par la


syntaxe pour générer le code intermédiaire correspondant à l'entrée à
analyser :
√ Trouver la grammaire "naturelle" pour reconnaître la construction ;
√ Définir le code intermédiaire correspondant à la construction ;
√ Définir correctement les routines sémantiques et les insérer aux "bons
endroits" de la grammaire ;
√ Transformer la grammaire si nécessaire ;
√ Utiliser toutes les structures de données aux routines sémantiques pour
générer le code intermédiaire.

VI.4. TRADUCTION DESCENDANTE

VI.4.1. Emplacement des routines sémantiques

Certaines grammaires sont "naturellement" récursives gauches


(comme par exemple la grammaire des expressions arithmétiques). Le
positionnement des routines sémantiques pour ces grammaires est
relativement simple. Par contre, si on prend une grammaire non récursive
gauche pour ces cas, le positionnement des routines sémantiques n'est pas
évident.
Le processus naturel consiste à donner la grammaire "naturelle",
positionner les routines sémantiques et enlever ensuite la récursivité gauche
pour pouvoir faire une traduction descendante.
Exemple :
 Schéma de traduction pour l'évaluation des expressions arithmétiques
avec récursivité gauche de la grammaire :
E → E1 + T {E.val := E1.val + T.val}
E → E1 - T {E.val := E1.val - T.val}
E→T {E.val := T.val}
T→(E) {T.val := E.val}
T → nb {T.val := nb.val}
132
6. Traduction Dirigée par la Syntaxe

 Le symbole E1 désigne en fait le non terminal E. Il a été utilisé juste pour


distinguer entre les occurrences droite et gauche de E.
 Pour effectuer une traduction descendante, le schéma de traduction
précédent ne convient pas. Il faut donc éliminer la récursivité à gauche et
"réadapter" les routines sémantiques. Le schéma de traduction suivant
convient à une traduction descendante :
E→ T {R.h := T.val}
R {E.val := R.s}
R→ +
T {R1.h := R.h + T.val}
R1 { R.s := R1.s}
R→ -
T {R1.h := R.h - T.val}
R1 { R.s := R1.s}
R→ε { R.s := R.h}
T→(E) {T.val := E.val}
T → nb {T.val := nb.val}
 Les attributs indicés .val ou .s sont des attributs synthétisés. Les attributs
indicés .h sont des attributs hérités.

VI.4.2. Cas général

 Considérer le schéma de traduction suivant, la grammaire est récursive


gauche, f et g sont des traitements quelconques et chaque symbole a un
attribut synthétisé :
A → A1 Y {A.s := g(A.s, Y.s)}
A→X {A.s := f(X.s)}
 Le schéma de traduction précédent ne convient pour une traduction
descendante. Nous devons d'abord éliminer la récursivité à gauche :
A→XR
R→YR|ε

133
6. Traduction Dirigée par la Syntaxe

 Le schéma de traduction adapté :


A→ X {R.h := f(X.s)}
R {A.s := R.s}
R→ Y {R1.h := g(R.h,Y.s)}
R1 {R.s := R1.s}
R→ ε {R.s := R.h}

VI.4.3. Conception d'un traducteur descendant

Donnée : Schéma de traduction adapté à l'analyse descendante.


Résultat : Code du traducteur descendant dirigée par la syntaxe.
 Considérer le schéma de traduction suivant, la grammaire est récursive
gauche, f et g sont des traitements quelconques et chaque symbole a un
attribut synthétisé :
 La génération du traducteur descendant est résumée par les étapes
suivantes :
√ Pour chaque non terminal A, construire une fonction ayant un
paramètre formel pour chaque attribut hérité et retournant toutes les
valeurs des attributs synthétisés ;
√ Ecrire le code de chaque fonction en suivant les principes donnés
dans le chapitre consacré aux analyses syntaxiques descendantes ;
√ Copier le code associé à toute action sémantique dans le corps de la
fonction à une position équivalente à sa position dans le schéma de
traduction.
Exemple 1 :
 Donner le schéma de traduction, dans le cas d'une analyse par descente
récursive, pour générer la forme post-fixée d'une expression
arithmétique.
 Grammaire récursive gauche générant les expressions arithmétiques :
E→E +T|T
T→T*F|F
F → ( E ) | id

134
6. Traduction Dirigée par la Syntaxe

 Schéma de traduction avec récursivité gauche de la grammaire :


E→E +T {Afficher (+)}
E→T
T→T*F {Afficher (*)}
T→ F
F→(E)
F → id
 Schéma de traduction pour convenir à une traduction descendante :
E→TR
R → + T {Afficher (+)} R
R→ε
T→FG
G → * F {Afficher (*)} G
G→ ε
F→(E)
F → id {Afficher (id)}
 Procédures (car dans l'exemple traité les attributs n'ont par de valeur
synthétisée) du traducteur descendant :
Procédure Z( )
Début
E( ) ;
Si tc ='#" Alors "Chaîne syntaxiquement correcte" Sinon "Erreur"
FinSi ;
Fin.
Procédure E( )
Début
T( ) ; R( ) ;
Fin.

135
6. Traduction Dirigée par la Syntaxe

Procédure R( )
Début
Si tc = '+'
Alors
tc = ts ; T( ) ; print('+'); R( ) ;
FinSi ;
Fin.
Procédure T( )
Début
F( ) ; G( ) ;
Fin.
Procédure G( )
Début
Si tc = '*'
Alors
tc = ts ; F( ) ; print('*'); G( ) ;
FinSi ;
Fin.
Procédure F( )
Début
Si tc = '('
Alors
tc = ts ;
E( ) ;
Si tc = ')' Alors tc = ts
Sinon "Erreur"
FinSi
Sinon
Si tc = 'id' Alors tc = ts ; print (nb) ;
Sinon "Erreur"
FinSi
FinSi
Fin.
Exemple 2 :
 Donner le schéma de traduction, dans le cas d'une analyse par descente
récursive, pour générer un code intermédiaire sous forme de quadruplets
pour les expressions logiques.

136
6. Traduction Dirigée par la Syntaxe

 Pour l'expression logique suivante "a or b and not c" on générera les
quadruplets suivants:
(not, c, , tmp1)
(and, b, tmp1, tmp2)
(or, a, tmp2, tmp3)
√ Grammaire non récursive gauche générant les expressions logiques :
E → T E'
E' → or T E' | ε
T → F T'
T' → and F T' | ε
F → not F | G'
G → ( E ) | id | t | f
√ Schéma de traduction :
E → T {E'.h := T.s}
E' {E.s := E'.s}
E' → or
T {E'1.h := f(E'.h,T.s}
E'1 {E'.s := E'1.s}
E' → ε {E'.s := E'.h}
T → F {T'.h := F.s}
T' {T.s := T'.s}
T' → and
F {T'1.h := g(T'.h,F.s}
T'1 {T'.s := T'1.s}
T' → ε {T'.s := T'.h}
F → not
F1 {F.s := h(F1.s)}
F → G {F.s := G.s}
G → ( E ) {G.s := E.s}
G → id {G.s := id.s}
√ Les traitements f, g et h seront explicitées dans le corps des fonctions du
traducteur descendant décrites ci-dessous :

137
6. Traduction Dirigée par la Syntaxe

Fonction E( )
Début
val := T( );
val' := E'(val);
return (val');
Fin.
Fonction E'( e'h)
Début
Si tc = or
Alors
val := T( ) ; i++ ;
Générer-Quadruplet (or, e'h, val, tmpi) ;
val := E'(tmpi);
return(val);
Sinon
return(e'h);
FinSi ;
Fin.
Fonction T( )
Début
val := F( );
val' := T'(val);
return (val');
Fin.
Fonction T'( t'h)
Début
Si tc = and
Alors
val := F( ) ; i++ ;
Générer-Quadruplet (and, t'h, val, tmpi) ;
val := T'(tmpi);
return(val);
Sinon
return(t'h);
FinSi ;
Fin.

138
6. Traduction Dirigée par la Syntaxe

Fonction F( )
Début
Si tc = not
Alors
tc = ts ;
val := F( ) ; i++ ;
Générer-Quadruplet (not, val, , tmpi) ;
return(tmpi);
Sinon
val := G( ); return(val);
FinSi ;
Fin.
Fonction G( )
Début
Si tc = (
Alors
tc = ts ;
val := E( ) ;
Si tc = )
Alors
tc = ts ; return (val);
Sinon "Erreur"
FinSi
Sinon
Si tc = id
Alors
val := tc; tc = ts ; return (val);
Sinon "Erreur"
FinSi
FinSi ;
Fin.

139
6. Traduction Dirigée par la Syntaxe

Exemple 3 :
 Donner le schéma de traduction, dans le cas d'une analyse par descente
récursive, pour générer un code intermédiaire sous forme de quadruplets
pour l'instruction if.
√ Grammaire factorisée générant l'instruction if :
<Instr-if> → if <Cond> then <Instr> <X>
<X> → else <Instr> | ε

√ Exemple d'instruction avec if imbriquées :


if Cond1 then if Cond2 then I1 else I2 ;

√ Forme intermédiaire à générer :

Quadruplets Cond1

JZ

Quadruplets Cond2

JZ

Quadruplets I1

Jump

Quadruplets I2

√ Fonctions du traducteur relatives à l'instruction if :

140
6. Traduction Dirigée par la Syntaxe

Fonction Instr-if( )
Début
Si tc = if
Alors
tc := ts;
Cond(); /* Quadruplets de condition */
Si tc = then
Alors
Quad(Qc) := <JZ, , , >;
Save-JZ := Qc; /* Save-JZ est une variable locale */
Qc++;
Instr(); /* appel de la fonction qui traite l'instruction qui suit then */
X(Save-JZ);
Sinon "Erreur";
FinSi ;
Sinon "Erreur";
FinSi ;
Fin.

Fonction X(Save-JZ)
Début
Si tc = else
Alors
tc := ts;
Quad(Qc) := <Jump, , , >;
Save-Jump := Qc++;
Quad(Save-JZ).4 := Qc;
Instr(); /* appel de la fonction qui traite l'instruction qui suit else */
Quad(Save-Jump).4 := Qc;
Else
Quad(Save-JZ).4 := Qc;
FinSi;
Fin.

141
6. Traduction Dirigée par la Syntaxe

VI.5. TRADUCTION ASCENDANTE

VI.5.1. Emplacement des routines sémantiques

A quel moment exécuter une routine sémantique lors d'une analyse


ascendante ?
 Les routines sémantiques sont exécutées lors des réductions c'est à dire
juste avant que le MDP (Membre Droit de Production) ne soit réduit.

VI.5.2. Définitions L-attribuées

Définition :
 Une définition dirigée par la syntaxe est dite L-attribuée si tout attribut
hérité de Xj (1≤j≤n)du MDP de la règle A → X1 X2 … Xn ne dépend que
:
√ des attributs des symboles X1 X2 … Xj-1
√ des attributs hérités de A.

VI.5.3. Elimination des actions intérieures

 Puisque les routines sémantiques sont exécutées lors des réductions, il


faudra éliminer toutes les actions intérieures d'un schéma de traduction
pour pouvoir une traduction dirigée par la syntaxe ascendante. Ceci peut
être effectué en utilisant le procédé suivant dans le cas d’une analyse de
type LR :
√ Utiliser des non terminaux marqueurs qui engendrent ε ;
√ Remplacer chaque action intérieure par un marqueur distinct ;
√ "Rattacher" l'action à la fin de la production M → ε où M est un
marqueur.
Exemple 1:
 Schéma de traduction utilisant des actions intérieures :
E→TR
R → + T {Afficher (+)} R
R → - T {Afficher (-)} R
R→ε
142
6. Traduction Dirigée par la Syntaxe

T → id {Afficher (id)}
 Schéma de traduction équivalent au précédent sans actions intérieures :
E → TR
R → + TMR | - TNR | ε
M→ ε {write ('+')}
N→ ε {write ('-')}
T → nb {write (id.val)}

VI.5.4. Schéma de traduction générant des quadruplets

VI.5.4.1. Traduction d'une instruction à choix multiple


 Traduction ascendante de l'instruction à choix multiple (ie case)
case E of
val 1 : inst 1;
val 2 : inst 2;
.
.
val n : inst n;
end_case
 Forme intermédiaire que l'on veut générer pour cette structure :
Code pour évaluer E dans tmp
Si Tmp ≠ val1 aller Etiq1
Code pour inst1
aller à Suite
Etiq1 : Si Tmp ≠ val2 aller à Etiq2
Code pour inst2
aller à Suite
:
Etiqn-2 : Si Tmp ≠ valn-1 aller à Etiqn-1
code pour instn-1
aller à Suite
Etiqn-1 : code pour Instn
143
6. Traduction Dirigée par la Syntaxe

suite :
Exemple d'instruction case et Forme Intermédiaire correspondante :
√ Instruction case :
Case var of
val1 : Instr1 ;
val2 : Instr2 ;
else : Instr3
end;
√ Forme Intermédiaire correspondante :
<:= , var, , tmp>
<- , tmp, val1 , >
JNZ

Quadruplets Instr1

Jump
<- , tmp, val2 , >
JNZ

Quadruplets Instr2

Jump

Quadruplets Instr3

Schéma de traduction :
√ Grammaire qui permet de générer les instructions case :
<inst-case> → case <exp> of <list> <default> end-case
<liste> → <list> val : instr ; | val : instr ;
<default> → else instr ;
√ On utilisera dans le schéma de traduction de l'instruction case, une
structure de données Tableau dénommée QUAD pour contenir les
quadruplets. Les éléments du tableau sont enregistrements à quatre
144
6. Traduction Dirigée par la Syntaxe

champs. On utilisera également une variable dénommée


QUADCOURANT qui pointe sur le tableau des quadruplets.
√ Schéma de traduction :
<inst-case> → case <expr> M1 of <liste> <default>
end case
<liste> → <liste> val : M2 instr ; M3 | val : M2 instr ;
M3
<default> → else instr M4;
M1 → ε { Quadruplets pour évaluer expression
dans tmp }
M2 → ε { QUAD(QUADCOURANT):=(-, val,
tmp, )
QUADCOURANT++ ;
save := QUADCOURANT ;
QUAD(QUADCOURANT++):=(JNZ, , , )
}

M3 → ε { Générer quadruplets des instructions ;


Empiler (pile-suite, QUADCOURANT) ;
QUAD(QUADCOURANT++):=(JMP, , , )
QUAD(save).2 := QUADCOURANT ;
}

M4 → ε { Générer quadruplets pour instruction ;


Tantque pile-suite non vide
Faire
Dépiler-dans (pile-suite, var) ;
QUAD(var).2 := QUADCOURANT ;
Fait;
}

145
6. Traduction Dirigée par la Syntaxe

VI.5.4.2. Traduction de l'instruction conditionnelle 'if'


√ Traduction de l'instruction if avec traitement d'imbrications.
Exemple d'instruction if et Forme Intermédiaire à générer :
√ Instruction :
if Cond1
then if Cond2
then Instr1
else Instr2
else if Cond3
then Instr3
else Instr4
√ Forme Intermédiaire à générer :

Quadruplets Cond1

JZ

Quadruplets Cond2

JZ

Quadruplets Instr1

Jump

Quadruplets Instr2

Jump

Quadruplets Cond3

JZ

Quadruplets Instr3

Jump

Quadruplets Instr4

146
6. Traduction Dirigée par la Syntaxe

Schéma de traduction :
√ Grammaire qui permet de générer les instructions case :
<inst-if> → if <cond> then <instr> else <instr>
| if <cond> then <instr>
√ Schéma de traduction :
<inst-if> → if <cond> M1 then <instr> M2 else <instr> M4
| if <cond> M1 then <instr > M3
M1 → ε { Quadruplets Cond1 ;
Empiler (Quadcourant, pile_JZ);
QUAD(QUADCOURANT++):=(JZ, , , ) }
M2 → ε { Dépiler (de pile_JZ dans Save);
Empiler (Quadcourant, pile_JMP);
QUAD(QUADCOURANT++):=(JMP, , , )
QUAD(Save).4 := Quadcourant; }
M3 → ε { Dépiler (de pile_JZ dans Save);
QUAD(Save).4 := Quadcourant; }
M4 → ε { Dépiler (de pile_Jump dans Save);
QUAD(Save).4 := Quadcourant; }
Remarques :
 Il peut exister plusieurs formes intermédiaires pour une même
construction. Il faudra choisir la représentation intermédiaire qui
s'exécute le plus rapidement.
 Dans une production "MGP récursive" faire attention à la position du
non terminal récursif car l'ordre de génération de code en dépend.
Exercice :
 Donner le schéma de traduction d'une expression conditionnelle du
langage C.
 Indications :
- la condition du langage C est toujours exprimée entre parenthèses
- Grammaire abrégée d'une expression conditionnelle de C :
Expr → Expr '||' T | T
T → T '&&' F | F
G → G '= =' R | G '!=' R | R
R → R '<' S | R '>' S | R '<=' S | R '>=' S | S
S → S '+' B | B
B → B '*' C | C
C → i | '(' Expr ')'

147
6. Traduction Dirigée par la Syntaxe

VI.5.4.3. Traduction des boucles Do-While et While-Do


Dans ce qui suit nous allons montrer la traduction des boucles Do-While et
While-Do en utilisant un même non terminal. La solution suivante n’utilise
pas de structures de données explicites mais exploite la pile d’analyse LR en
supposant que chaque symbole de la grammaire empilé possède des
attributs.
Exemple d'instruction if et Forme Intermédiaire à générer :
√ Instruction :
while cond1
do
{ instr1
do { instr2 ;
while cond2
do instr3;
}
while cond3 ;
instr4 ;
}
√ Forme Intermédiaire à générer :

Quadruplets cond1
JZ
Quadruplets instr1

Quadruplets instr2

Quadruplets cond2
JZ
Quadruplets instr3
Jump
Quadruplets cond3
JNZ
Quadruplets instr4
JUMP

148
6. Traduction Dirigée par la Syntaxe

Schéma de traduction :
√ Grammaire qui permet de générer les instructions case :
<boucles-w> → while <cond> do <instr>
| do <instr> while <cond>
√ Schéma de traduction :
<boucles-w> → while M1 <cond> M2 do <instr> M3
| do M4 <instr> while <cond> M5
M1 → ε { M1.val = QUADCOURANT; }
M2 → ε {
QUAD(QUADCOURANT++):=(JZ, , , ) ;
M2.val:= (QUADCOURANT-1);
}
M3 → ε {
QUAD(QUADCOURANT++):=(JUMP, , ,M1.val ) ;
QUAD(M2.val).4 := QUADCOURANT;
}
M4 → ε { M4.val = QUADCOURANT; }
M5 → ε {
QUAD(QUADCOURANT++):=(JNZ, , ,M4.val ) ;
}

VI.6. YACC

VI.6.1. Grammaires YACC

 Yacc (Yet Another Compiler Compiler) est un programme destiné à


compiler une grammaire du type LALR(1) et à produire le texte source
d'un analyseur syntaxique du langage engendré par cette grammaire. Il
est aussi possible, en plus de la vérification de la syntaxe de la
grammaire, de lui faire effectuer des actions sémantiques.

VI.6.2. Structure d'un programme YACC

 De la même manière que pour un fichier Lex, un fichier Yacc se


compose de trois parties, de cette façon :

149
6. Traduction Dirigée par la Syntaxe

déclarations
%%
productions
%%
code additionnel

Remarque :
 Seul le premier séparateur %% et la deuxième partie étant obligatoires.

VI.6.2.1. La première partie d'un fichier Yacc


La première partie d'un fichier Yacc peut contenir :
 Des spécifications écrites dans le langage cible, placées entre %{ et
%}, ces deux symboles étant obligatoirement en début de ligne.
 La déclaration des terminaux pouvant être rencontrés, grâce au mot-
clé %token
 Le type de donnée du terminal courant, avec le mot-clé %union
 Des informations donnant la priorité et l'associativité des opérateurs.
 L'axiome de la grammaire, avec le mot-clé %start (si celui-ci n'est
pas précisé, l'axiome de la grammaire est le MGP de la première
production de la deuxième partie).
La variable yylval, déclarée implicitement du type de %union a une
importance fondamentale dans le fichier, puisque celle-ci contient la
description du dernier terminal lu.

VI.6.2.2. La deuxième section d'un fichier Yacc


Cette partie, qui ne peut pas être vide, contient les productions de la
grammaire du langage choisi.
Ces productions s'écrivent sous la forme générale :

non_terminal:
corps_1 { action_sémantique_1 }
| corps_2 { action_sémantique_2 }
| ...
| corps_n { action_sémantique_n }
;

150
6. Traduction Dirigée par la Syntaxe

sachant que les corps_i peuvent être des symboles terminaux ou non
terminaux de la grammaire.

VI.6.2.3. La troisième partie d'un fichier Yacc


Cette partie, qui comporte le code additionnel, devra obligatoirement
comporter une déclaration du main() (qui devra appeler la fonction
yyparse()), et de la fonction yyerror(char *message), appelée lorsqu'une
erreur de syntaxe est trouvée.

VI.6.3. Exemple de programme YACC

 Le programme YACC suivant permet d'évaluer une expression


arithmétique donnée en entrée :
%{
#include <ctype.h>
%}
%token chiffre
%%
Ligne : Expr '\n' { printf("%d \n", $1); }
Expr : Expr '+' Terme { $$ = $1 + $3; }
| Terme
;
Terme : Terme '+' Facteur { $$ = $1 * $3; }
| Facteur
;
Facteur : '(' Expr ')' { $$ = $2; }
| chiffre
;
%%
yylex()
{ int c;
c = getchar();
if (isdigit(c))
{ yylval = c – '0';
return chiffre ;
}
return (c);
}
151
6. Traduction Dirigée par la Syntaxe

VI.6.4. Variables et commandes de YACC


 Les terminaux d'une grammaire spécifiée dans la partie deux d'un
programme YACC sont entre ' ' ou les noms déclarés comme entités
lexicales. Les non terminaux sont les chaînes qui ne sont pas entre ' ' et
non déclarés comme entités lexicales.
 $$ désigne l'attribut associé au MGP (Membre Gauche de Production
d'une règle).
 $i désigne l'attribut associé au ième symbole d'un MDP (Membre Gauche
de Production d'une règle).
 La règle sémantique par défaut est {$$ = $1}.
 La production vide est représenté par une alternative vide.
 On peut spécifier dans un fichier YACC une grammaire ambiguë. Mais
il faut à ce moment lever les ambiguïtés en utilisant des conventions.
YACC permet de spécifier les associativités et priorités des opérateurs
dans la partie déclaration. Les associativités sont spécifiées par les mots
clés Left (pour une associativité gauche) ou Right (pour une associativité
droite). Les opérateurs ainsi spécifiés auront des priorités croissantes
selon l'ordre d'écriture.

VI.6.5. Fonctionnement de l'analyseur

VI.6.5.1. Table de l'analyseur


 L'analyseur effectue à chaque étape une des quatre actions possibles
suivantes :
shift
reduce
accept
error
 Si YACC est invoqué avec l'option –v (sous Unix) génère un fichier
y.output qui contient une description lisible de la table d'analyse.
Exemple :
 Fichier.y
%token DO RE MI
%%
rhyme : sound place
;
sound : DO RE
;
place : MI
;
152
6. Traduction Dirigée par la Syntaxe

 y.output
0 $accept : rhyme $end
1 rhyme : sound place
2 sound : DO RE
3 place : MI
^L
state 0
$accept : . rhyme $end (0)
DO shift 1
. error
rhyme goto 2
sound goto 3
state 1
sound: DO . RE (2)
RE shift 4
. error
state 2
$accept : rhyme . $end (0)
$end accept
. error
state 3
rhyme : sound . place (1)
MI shift 5
. error
place goto 6
state 4
sound: DO RE . (2)
. reduce 2
state 5
place : MI . (3)
. reduce 3
state 6
rhyme : sound place . (1)
. reduce 1
5 terminals, 4 non terminals
4 grammar rules, 7 states

153
6. Traduction Dirigée par la Syntaxe

Exercice :
 Vérifier la table d'analyse LALR(1) précédente en la construisant vous-
même. Commentez.
Eléménts de réponse :
 Items LR(0) après rajout de la règle S' → rhyme # :
√ I0 = {[S' → .rhyme], [rhyme → .sound place], [sound → .DO RE]}
√ I1 = GOTO(I0, rhyme) = {[S' → rhyme.]}
√ I2 = GOTO(I0, sound) = {[rhyme → sound . place], [place → .MI]}
√ I3 = GOTO(I0, DO) = {[sound → DO . RE]}
√ I4 = GOTO(I2, place) = {[rhyme → sound place .]}
√ I5 = GOTO(I2, MI) = {[place → MI .]}
√ I6 = GOTO(I3, RE) = {[sound → DO RE . ]}
 Table d'analyse SLR(1) :

DO RE MI # rhyme sound place


0 D3 1 2
1 Accept
2 D5 4
3 D6
4 R(1)
5 R(3)
6 R(2)

VI.6.5.2. Résolution des conflits dans YACC


Priorité des terminaux :
 Les priorités et associativités sont définies dans la partie déclaration. On
spécifie les terminaux des moins prioritaires vers les plus prioritaires.
Les terminaux de même priorité sont spécifiés sur la même ligne. Un
exemple est donné ci-après :
%left '+', '-'
%left '*', '/'
%right moins_unaire

154
6. Traduction Dirigée par la Syntaxe

Priorité d'une règle :


 La priorité d'une règle est la priorité du terminal le plus à droite du MDP
(membre droit de production). On peut forcer la priorité d'une règle par
la commande : %prec valeur.
Grammaire ambiguë pour les expressions arithmétiques :
 Résolution des conflits par l'utilisation des priorités et associativités
comme dans l'exemple ci-après :
%left '+', '-'
%left '*', '/'
%right moins_unaire
%%
Expr : Expr '+' Expr
| Expr '-' Expr
| Expr '*' Expr
| Expr '/' Expr
| '-' Expr prec
moins_unaire
;
Résolution des conflits Décaler/Réduire :
 Si conflit entre Décaler a et Réduire par A → α, Yacc effectue :
- une réduction si :
 priorité(règle) > priorité(a) ;
 même priorité et associativité à gauche.
- un décalage dans tous les autres cas.
Résolution des conflits Réduire/Réduire :
 Placer la production de réduction préférée en premier dans l'ordre
d'apparition des règles.

VI.6.5.3. Utilisation commune de LEX et YACC


Le programme principal du compilateur (l'analyseur) doit appeler la
fonction yyparse pour lancer l'analyse. La fonction yyparse fait appel à la
fonction yylex chaque fois qu'elle a besoin d'une entité lexicale.

155
6. Traduction Dirigée par la Syntaxe

 programme principal de l'analyseur :


main ()
{ …
yyparse();

}
 Le schéma général de construction d'un compilateur est donné ci-après :

 Schéma de fonctionnement interne :

 Commandes pour construire un analyseur (options selon le système


Unix) ;:
$ lex fichier.l
$ yacc –d fichier.y
$ cc –o analyseur
y.tab.c lex.yy.c -ll

156
6. Traduction Dirigée par la Syntaxe

Mnémoniques d'instructions de branchement


 Mnémoniques à utiliser pour les étiquettes de branchements dans tous
les exercices le nécessitant pour la production du code intermédiaire.
 Les mnémoniques suivantes sont ceux utilisées dans les instructions
machines des processeurs Intel.
Nom Jump If Indicateurs testés
Comparaison à Zéro
JZ Zero Z_flag = 1
JNZ Non zero Z_flag = 0
Comparaison Nombres non signés
JA Above (C et Z) = 0
JB Below C=1
JAE Above or Equal C=0
JBE Below or Equal (C ou Z) = 1
JNC No Carry C=0
Comparaison Nombres signés
JG Greater Z = 0 et S = O_flag
JL Less S ≠ O_flag
JGE Greater or Equal S = O_flag
JLE Less or Equal Z = 1 ou (S ≠ O_flag)
Test d'overflow
JO O_flag = 1
JNO O_flag = 0
Test de signe
JS Sign S=1
JNS No Sign S=0
Test de parité
JPO Parity Odd P=0
JPE Parity Even P=1

157
6. Traduction Dirigée par la Syntaxe

VI.7. EXERCICES

Exercice 1.
 Traduire l'expression – (a + b) * (c +d) + (a +b +c) en :
a) quadruplets ;
b) triplets
c) triplets indirects

Exercice 2.
 Traduire les instructions suivantes sous forme de notation polonaise
préfixée :
1. a – b * c + (a + b)
2. a + (b -c)*(a - b)*b/(c + 2) - ((a - b)**3 + 2)*c + a

Exercice 3.
• Traduire les instructions du programme C suivant :
main()
{ int i;
int a[10];
i=0;
while (i<10)
{ a[i] = 0;
i = i + 1; } }
en :
a) un arbre abstrait ;
b) une notation postfixée ;
c) un code à trois adresses.

Exercice 4.
 Ecrire les routines sémantiques dans le cas d’une analyse descendante
(descente récursive) générant des quadruplets de :
a) L’expression logique et les opérateurs AND, OR et NOT.
b) L’instruction REPEAT PASCAL.
Forme générale :
REPEAT instruction UNTIL condition.
c) L’instruction For du langage C
158
6. Traduction Dirigée par la Syntaxe

Nb :
 Prendre en compte les imbrications de structures dans les cas b) et c).

Exercice 5.
a) Ecrire les routines sémantiques dans le cas d’une analyse descendante
(descente récursive) pour évaluer les expressions arithmétiques avec
parenthèses. Utiliser obligatoirement les attributs synthétisés et les
attributs hérités dans votre schéma de traduction.
b) Appliquer votre schéma pour évaluer l'expression suivante : 5 + 10 * (9
– 4).

Exercice 6.
 Considérer l'instruction suivante permettant de calculer la variance de
plusieurs expressions arithmétiques :
id := Variance (<Exp1>, <Exp2>, …, <Expn>)
a) Donner la grammaire permettant de générer l'instruction d'affectation
décrite ci-dessus (n≥1).
b) Donner le schéma de traduction sous forme de quadruplets dans le cas
d'une analyse descendante.

Exercice 7.
a) Ecrire les routines sémantiques dans le cas d’une analyse descendante
(descente récursive) pour générer les quadruplets correspondant à une
expression de condition.
 Utiliser obligatoirement les attributs synthétisés et les attributs hérités
dans votre schéma de traduction.
 Utiliser les priorités et associativités classiques des opérateurs
logiques et arithmétiques. Les opérateurs logiques sont moins
prioritaires que les opérateurs relationnels qui sont eux moins
prioritaires que les opérateurs arithmétiques.
b) Appliquer votre schéma pour générer les quadruplets correspondant à
l'expression suivante:
(( a + b >= 5) or ( c < a – b * 4) and (a > d))
Exercice 8.
 Soit l’instruction CASE PASCAL. Donner le schéma de traduction de
l’instruction dans le cas d’une analyse descendante générant des
quadruplets.
159
6. Traduction Dirigée par la Syntaxe

Exercice 9.
• Soit l’instruction SELECT dont la forme générale est :
SELECT
<liste-val-1> : <liste-inst-1>;
<liste-val-2> : <liste-inst-2>;
: :
<liste-val-n> : <liste-inst-n>;
BY <expr>;

SELECT et BY sont des mots réservés;


<liste-val-i> est une liste de valeurs (val-i1, val-i2,…,val-im) avec m ≥ 1;
<liste-inst-i> est une liste d’instructions éventuellement terminée par
l’instruction EXIT;
<expr> est une expression arithmétique;
Interprétation :
<expr> est comparée séquentiellement aux valeurs des <liste-val-i> (1 ≤ i ≤
n); si <expr> est égale à une valeur de <liste-val-i> alors <liste-inst-i> est
exécutée; si l’instruction EXIT est présente dans <liste-inst-i> alors
l’instruction SELECT se termine , sinon la comparaison de <expr> avec les
valeurs des <liste-val> restantes se poursuit.
• Donner le schéma de traduction (routines sémantiques) de l’instruction
SELECT dans le cas d’une analyse ascendante générant des quadruplets.

Exercice 10.
 Soit l'instruction Select suivante (n ≥1):
Select max of Select min of
<exp1> : <inst1>; <exp1> : <inst1>;
<exp2> : <inst2>; <exp2> : <inst2>;
: :
<expn> : <instn> <expn> : <instn>
end; end;
où Select, min, max, of et end sont des mots réservés ;
<insti> est une liste d’instructions ;
<exp> est une expression arithmétique (entière ou réelle).
Fonctionnement :
 L'instruction Select sélectionne les instructions telles que l'expression
correspondante est égale au maximum (resp. au minimum) des
160
6. Traduction Dirigée par la Syntaxe

expressions <exp1>, <exp2>, ...<expn>. Les instructions sélectionnées


sont alors exécutées et l'instruction Select se termine. Si deux
expressions (ou plus) sont égales au maximum (resp. au minimum) des
expressions <exp1>, <exp2>, ...<expn>, prendre la première dans l'ordre
d'apparition dans le texte.
Question :
• Donner le schéma de traduction sous forme de quadruplets dans le cas
d'une analyse ascendante :
• la forme du code intermédiaire à générer ;
• la grammaire syntaxique ;
• les attributs associés aux symboles de la grammaire ;
• les routines sémantiques associées aux productions de la
grammaire.

Exercice 11.
 Donner le programme YACC qui permet d'afficher la forme postfixée
d'une expression arithmétique lue en entrée.
Exemple : Si on lit : 5+4*3-2 alors on affiche : 5 6 3 * + 2 –
Nb :
 Chaque opérande est un nombre.
 Pas de moins unaire dans les expressions.
 Considérer également les expressions avec parenthèses.

Exercice 12.
 Ecrire un programme YACC qui permet de construire l'arbre syntaxique
d'une expression arithmétique.
Nb :
 Considérer les expressions avec parenthèses.
 Chaque opérande est un nombre.
 Considérer également le moins unaire dans les expressions.
Indication :
 On dispose d'une fonction C node() appelée avec trois arguments.
 L'appel node( L, n1, n2 ) crée un nœud avec le label (ou étiquette L)
et deux descendants n1 et n2 et retourne l'adresse de la structure
nouvellement créée.
 Exemple d'utilisation : expr : expr '+' expr { $$ = node( '+', $1,
$3 ); }

161
6. Traduction Dirigée par la Syntaxe

Exercice 13.
 Considérer le programme YACC suivant :
%left '+' '-'
%left '*' '/'
↑'
%right '↑
%%
expr : expr ↑'
'↑ expr
| expr '+' expr
| expr '-' expr
| expr '*' expr
| expr '/' expr
| NAME
;
 Comment seront interprétées les expressions suivantes ; donner leurs
arbres syntaxiques :
a + c * d ↑ d ↑ b - e ↑ f * g
(a + b) * c ↑ d ↑ e - f ↑ g / h

Exercice 14.
 Donner un programme YACC qui utilise une grammaire ambiguë pour
analyser et évaluer des expressions arithmétiques utilisant les opérateurs
classiques +, *, -, / et l'opérateur de puissance désigné par ^. L'opérateur
^ a la priorité et associativité classiques dans les expressions
mathématiques. On suppose qu'on dispose d'une fonction C power (a,b)
qui permet de calculer ab (a à la puissance b).

162
7. Environnements d'exécution

CHAPITRE VII. ENVIRONNEMENTS


D'EXECUTION

VII.1. INTRODUCTION
Avant d'entamer la phase de production de code, il faut établir le rapport
entre le texte source, statique, d'un programme et les actions qui doivent être
effectuées à l'exécution pour implanter ce programme. Lors de l'exécution,
un même nom dans le texte source peut dénoter des données différentes
dans la machine cible. Dans ce chapitre, nous nous intéresserons aux
relations qui existent entre noms et données.
L'allocation et la libération des données sont gérées par le paquetage
de soutien d'exécution, consistant en des routines chargées avec le code cible
produit. La conception du paquetage de soutient d'exécution est influencée
par la sémantique des procédures.

VII.2. PROCEDURES ET ACTIVATIONS

VII.2.1. Procédures

Une procédure est définie, statiquement, par une identification et un


corps (on parle de corps de procédure). L'identificateur est le nom de la
procédure et le corps une suite d'instructions.
Une activation de procédure correspond à un appel de cette procédure
lors de l'exécution.
On distinguera comme types de procédure :
 Les procédures avec ou sans paramètres ;
 Les fonctions : qui sont des procédures qui retournent une valeur.
Remarque :
• Dans le reste de ce chapitre, on utilisera le terme de procédure pour
désigner une procédure ou une fonction.

163
7. Environnements d'exécution

Il faut bien distinguer le texte source d'une procédure et ses activations


possibles à l'exécution. A un texte source peut correspondre une ou plusieurs
activations. Si une procédure est récursive, plusieurs de ses activations
peuvent coexister. A chaque activation correspond un environnement
d'exécution (données, état de la machine). La représentation d'une donnée à
l'exécution est déterminée par son type.
Exemple :
 Texte source d'un programme Pascal triant des entiers dans un tableau
par la méthode QuickSort :
Program Trier(input, output);
var t: array [1..9] of integer;
procedure LireTableau;
var i : integer;
begin for i:=1 to 9 do read (t[i]); end;
function Partition (y,z : integer) : integer;
var i,j,k,x : integer; u : array[y,z] of integer;
begin
for i:=y to z do u[i]:=t[i];
x:= t[y]; j:=y; k:=z;
for i:=y+1 to z
do if u[i]<x
then begin t[j]:=u[i]; j:=j+1; end
else begin t[k]:=u[i]; k:=k-1; end ;
Partition := j;
/* fonction qui détermine le pivot du tableau */
/* en sortie de cette fonction, le pivot sera à sa bonne place dans le
tableau */
end;

164
7. Environnements d'exécution

procedure TriRapide (m,n : integer);


var i: integer;
begin
if n>m
then
begin
i:= Partition(m,n);
TriRapide(m,i-1);
TriRapide(i+1,n);
end
end;
Begin
LireTableau;
TriRapide(1,9);
End.

VII.2.2. Arbre d'activation

Durée de vie d'une activation :


 La durée de vie d'une activation d'une procédure P est la séquence des
étapes entre l'appel de la procédure P et le retour depuis cette procédure.
Relation entre durées de vie d'activations :
 Si A1 et A2 sont deux activations de procédures, leurs durées de vie sont
soit disjointes soit imbriquées.
Arbre d'activation :
 L'arbre d'activation est une structure représentant toutes les activations
d'un programme donné. Chaque nœud (sommet) de cet arbre représente
une activation de procédure. La racine de l'arbre représente l'activation
du programme principal.
Remarque :
 L'arbre d'activation n'est jamais construit explicitement car seulement
une branche de cet arbre est "vive" à un moment donné.

165
7. Environnements d'exécution

Exemple :
 Arbre d'activation correspondant à l'exécution du programme de la
section 2.1 (l'étiquette T correspond à TriRapide et P à Partition) :

Trier

LireTableau T(1,9)

P(1,9) T(1,3) T(5,9)

P(1,3) T(1,1) T(3,3) P(5,9) T(5,5) T(7,9)

P(7,9) T(7,7) T(9,9)

VII.2.3. Pile de contrôle

Flot de contrôle :
 Le flot de contrôle durant l'exécution d'un programme donné correspond
à un parcours en profondeur de l'arbre d'activation.
Pile :
 Pour garder trace des activations de procédures encore vives, la pile de la
machine est utilisée. Cette pile est appelée pile de contrôle. La gestion
des activations vives en pile permet de connaître facilement la portée des
noms. Pour chaque activation, on gardera un ensemble d'informations
(explicité ultérieurement).

Exemple :
 Pendant de l'exécution du programme de la section 2.1, si l'appel
TriRapide(3,3) est en cours de traitement, le contenu de la pile sera le
suivant :

166
7. Environnements d'exécution

Trier
TriRapide (1,9)
TriRapide (1,3)
TriRapide (3,3)
Sommet de pile

VII.3. ORGANISATION DE L'ESPACE MEMOIRE

VII.3.1. Répartition de la mémoire à l'exécution

Au cours de l'exécution d'un programme, l'espace mémoire centrale


associé est organisé en zone de code et zone de données.
Zone de code :
 La zone de code correspond aux instructions machines correspondant au
programme source.
Zone de données :
 La zone de données englobe toutes les données nécessaires au
fonctionnement du programme. On peut distinguer dans la zone de
données, les données statiques dont la taille est connue à la compilation
et les données dynamiques dont l'allocation se fera au cours de
l'exécution. La pile (stack) et le tas (heap en anglais) stockent les
données dynamiques d'une exécution.
√ La pile garde trace des différentes activations.
√ Le tas sert pour l'allocation dynamique des données.

167
7. Environnements d'exécution

Code
taille connue à la compilation
Données statiques

Pile

Tas

Remarque :
 Le sens de croissance de la pile et du tas (illustrés par le tableau
précédent) est une convention prise par certains concepteurs de machine.

VII.3.2. Bloc d'activation

Définition:
 Un bloc d'activation ou enregistrement d'activation est un bloc mémoire
contenant les informations nécessaires à l'exécution d'une procédure. Le
bloc d'activation est structuré en plusieurs parties.
Exemple d'organisation :
 Le schéma suivant illustre, un exemple d'organisation du bloc
d'activation en différentes parties :
Adresse de retour
Paramètres effectifs
Lien de contrôle
Lien d'accès
Etat machine sauvegardé

Données locales

Temporaires

Remarques :

168
7. Environnements d'exécution

i) Certaines structurations du bloc d'activation ne nécessitent pas tous ces


champs.
ii) Certains champs du bloc d'activation sont souvent stockés dans des
registres.
Rôle des différents champs du bloc d'activation :
 Adresse de retour :
Contient l'adresse de retour vers la procédure appelante.
 Paramètres effectifs :
Paramètres utilisés par la procédure appelante pour fournir des
arguments à la procédure appelée.
 Lien de contrôle :
repère le bloc d'activation de la procédure appelante.
 Lien d'accès :
référence des données non locales (contenues dans d'autres blocs
d'activation).
 Etat machine sauvegardé :
l'état de la machine avant l'appel de la procédure (valeurs registres,
…)
 Données locales :
Toutes les données locales à l'exécution de la procédure.
 Temporaires :
Valeurs temporaires utilisées pendant l'exécution de la procédure.
Remarques :
i) La plupart des tailles des champs du bloc d'activation sont connues à la
compilation.
ii) Une des exceptions : les variables de type tableau déclarés dans le corps
d'une procédure, dont la taille n'est connue qu'à l'exécution.

VII.4. ALLOCATION DE LA MEMOIRE

VII.4.1. Allocation statique

Les données du programme dont la taille est connue à la compilation,


sont organisées avant l'exécution du programme. C'est au compilateur de
déterminer la quantité de mémoire à réserver pour un nom à partir du type
associé à ce nom.
Exemple :
169
7. Environnements d'exécution

Type Espace associé


char 1 octet
short 2 octets
int 4 octets

VII.4.2. Allocation en pile

VII.4.2.1. Principe
Un bloc d'activation d'une procédure est empilé lorsque la procédure est
appelée et il est dépilé lorsqu'elle se termine. Un des registres de la machine
est dédié à la gestion de la pile. Il s'agit du registre SP (Stack Pointer) qui
pointe sur le sommet de la pile.
Avant d'exécuter une procédure, son bloc d'activation est empilé en
mémoire et le sommet de pile est incrémenté de la taille du bloc d'activation.
Après le retour de la procédure, le sommet de pile est décrémenté de la taille
du bloc.
Exemple d'évolution de la pile :
 L'exemple suivant illustre l'évolution de la pile au cours de l'exécution
du programme de la section 2.1 :
Position dans l'arbre Blocs d'activation en pile
d'activation *

Trier* Trier

Trier
Trier
LireTableau* LireTableau

Trier
TriRapide (1,9) Trier
TriRapide(1,9)
TriRapide (1,9)*
TriRapide(1,3)

170
7. Environnements d'exécution

VII.4.2.2. Protocoles d'appel


Les appels de procédure sont implantés par la production dans le code
cible de ce qu'on appelle des protocoles d'appel. Une séquence d'appel alloue
un bloc d'activation et remplit ses champs avec les informations appropriées.
Une séquence de retour restaure l'état de la machine afin de permettre à la
procédure appelante de continuer son exécution.
Remarque :
 Les protocoles d'appel et les blocs d'activation ne sont pas toujours
implantés de la même manière selon les langages (ou même dans des
implantations différentes d'un même langage).
Responsabilités lors des appels :
 Il n'y a pas de séparation bien définie entre les responsabilités de
l'appelant et l'appelé au cours d'une exécution. L'exemple suivant montre
une séparation des tâches entre appelé et appelant :

paramètres et
valeurs de retour
lien de contrôle bloc d'activation
sauvegarde de l'état de l'appelant
données locales et
temporaires responsabilité
paramètres et appelant
valeurs de retour
lien de contrôle bloc d'activation
sauvegarde de l'état responsabilité de l'appelé
données locales et appelé
temporaires

Exemple de séquence d'appel :


 L'appelant évalue les arguments ;
 L'appelant stocke dans le bloc d'activation de l'appelé l'adresse de retour
et l'ancienne valeur de sommet_pile ;

171
7. Environnements d'exécution

 L'appelé incrémente ensuite sommet_pile ;


 L'appelé sauvegarde l'état courant (les valeurs des registres, … ) ;
 L'appelé initialise ses données locales et commence son exécution.

Exemple de séquence de retour :


 L'appelé place une valeur de retour s'il s'agit d'une fonction ;
 L'appelé restaure le sommet de pile et d'autres registres ;
 L'appelé se branche à une adresse de retour dans le code de l'appelant.

VII.4.2.3. Données de taille variable


Dans cette section, nous nous intéresserons au problème du stockage des
données de taille variable déclarées dans une procédure. L'exemple typique
est donné par les tableaux déclarés dans une procédure et dont la taille n'est
connue qu'à l'exécution.

Stratégie d'allocation :
Les données de ce type doivent être dans la pile puisque après le retour
d'une procédure ils n'ont plus d'existence. Ces données ne peuvent être
stockées dans le bloc d'activation car (en général) l'espace réservé à un bloc
d'activation est fait durant la compilation.
Ces données sont stockées en fait juste après le bloc d'activation de la
procédure appelée qui les "contient". Dans ce bloc d'activation, doit figurer
impérativement des pointeurs vers ces données (la taille des pointeurs est
déterminé à la compilation).

Exemple :
 Une procédure P ayant trois tableaux locaux A, B, C est appelée. Cette
procédure appelle une procédure Q. Le schéma suivant illustre l'état de
la pile pendant l'exécution de la procédure Q :

172
7. Environnements d'exécution

lien de contrôle bloc d'activation


pointeur vers A de P
pointeur vers B
pointeur vers C

Tableau A

Tableau B Tableaux de P

Tableau C

lien de contrôle bloc d'activation


de Q

Tableaux de Q

VII.4.3. Allocation dans le tas

Le tas est une zone de données indispensable pour l'allocation dynamique


des données. Une procédure peut allouer dynamiquement des données
pendant son activation. Si ces données doivent persister après le retour de la
procédure, on ne peut les stocker en pile. Car en faisant cela, les données
disparaîtront avec la fin de l'exécution de la procédure. Ces données sont
donc stockées dans une structure appelée Tas et qui permet la persistance
des données.
Il est une précaution à prendre cependant, c'est de libérer les
dynamiques qui ne seront plus utilisées. Cette libération peut se faire
explicitement par des instructions du langage ou implicitement par un
programme appelé ramasse-miettes (Garbage Collector) qui se déclenche
périodiquement.

173
7. Environnements d'exécution

VII.5. ACCES AUX NOMS NON LOCAUX

Règles de portée :
 Les règles de portée d'un langage déterminent le traitement à effectuer
pour les références à des noms non locaux. Une règle courante, appelée
règle de portée statique ou lexicale, permet de déterminer quelle
déclaration s'applique à un nom par le seul examen du texte source du
programme.
 Les langages Pascal, C et Ada font partie des nombreux langages qui
utilisent la portée statique, en ajoutant la règle de "l'englobant le plus
imbriqué".

VII.5.1. Blocs

Un bloc est une instruction contenant ses propres déclarations de données


locales. Ce concept est utilisé par le langage C. En C, la syntaxe d'un bloc
est :
{
déclarations
instructions
}

Remarques :
• Les blocs peuvent être imbriqués ou disjoints. Il n'y a jamais de
chevauchement entre deux blocs.
• La portée dans les langages à structure de blocs est donnée par la règle
de l'englobant le plus imbriqué.
Portée d'une déclaration :
 La portée d'une déclaration dans un bloc B inclut B.
 Si un nom x n'est pas déclaré dans un bloc B, une occurrence de x dans
B est dans la portée d'une déclaration de x dans un bloc englobant B' tel
que :
i) B' comporte une déclaration de x et,
ii) B' est le plus imbriqué des blocs contenant B et ayant une déclaration
de x.

174
7. Environnements d'exécution

Exemple :
 Blocs dans un programme C :
main ( )
{
int a = 0;
int b = 0;
{
int b = 1;
{
int a = 2;
printf("%d %d\n", a , b);
}
{
int b = 3;
printf("%d %d\n", a , b);
}
printf("%d %d\n", a , b);
}
printf("%d %d\n", a , b);
}

Implantation de la structure de blocs :


 La structure de blocs peut être implantée en utilisant une allocation en
pile. Puisque la portée d'une déclaration ne s'étend pas à l'extérieur du
bloc dans lequel elle apparaît, l'emplacement réservé au nom déclaré
peut être alloué à l'entré du bloc et libérée à la sortie. Cette façon de voir
les choses traite les blocs comme des procédures sans paramètres (en
beaucoup plus simple).

VII.5.2. Portée statique sans déclaration de procédures


imbriquées
Les règles de portée statique du langage C sont plus simples que celles du
langage Pascal car les définitions de procédures ne peuvent être imbriquées
en C. Une définition de procédure ne peut apparaître à l'intérieur d'une autre.
S'il y a une référence non locale à un nom X dans une procédure quelle
qu'elle soit, alors X doit être déclaré à l'extérieur de toute fonction.
En l'absence de procédures imbriquées, la stratégie d'allocation en pile
pour les noms locaux présentée à la section 4.2 peut être utilisée. Les
emplacements pour tous les noms déclarés à l'extérieur de toute procédure
peuvent être alloués statiquement. Tout autre nom doit désigner une variable
locale de l'activation en sommet de pile.

175
7. Environnements d'exécution

VII.5.3. Portée statique avec déclaration de procédures


imbriquées
Une occurrence non locale d'un nom X dans une procédure Pascal est
dans la portée de la déclaration de X englobante la plus imbriquée dans le
texte source.
Profondeur d'imbrication :
 La notion de profondeur d'imbrication est définie comme suit : soit 1 la
profondeur d'imbrication du programme principal ; nous ajoutons 1 à la
profondeur d'imbrication lorsqu'on pénètre dans une procédure englobée
depuis sa procédure englobante.
Exemple :
 Dans le programme suivant, les occurrences de t, v et i dans Partition ont
pour profondeur d'imbrication respectivement 1, 2 et 3.
Program Trier(input, output);
var t : array [1..9] of integer;
x : integer
procedure LireTableau;
var i : integer;
begin
…t…
end;
procedure Echanger (i,j : integer) ;
begin
x := t[i] ; t[i] := t[j] ; t[j] := x;
end;
procedure TriRapide (m,n : integer);
var k,v: integer;
function Partition (y,z : integer) : integer;
var i,j,: integer;
begin
…t…
…v…
… Echanger(i,j); …
end;
begin

end;
Begin

End.

176
7. Environnements d'exécution

Lien d'accès :
 Une implantation directe de la portée statique pour les procédures
imbriquées est obtenue en ajoutant à chaque bloc d'activation, un
pointeur appelé lien d'accès. Si une procédure P est imbriquée
immédiatement dans une procédure T dans le texte source, le lien
d'accès d'un bloc d'activation de P référence le lien d'accès du bloc
associé à l'activation la plus récente de T.
Comment retrouver une donnée non locale ?
 Une procédure P, qui a une profondeur d'imbrication Np, référence une
donnée non locale t de profondeur d'imbrication Nt avec Nt ≤ Np. Pour
retrouver cette donnée, il suffit de suivre Np – Nt liens d'accès.

VII.5.4. Portée dynamique

Les règles de portée dynamique, détermine lors de l'exécution quelle


déclaration s'applique à un nom en considérant les activations en cours.
LISP est un langage qui utilise la portée dynamique
Dans le cas de la portée dynamique, la liaison des noms non locaux avec
des emplacements en mémoire n'est pas changée lorsqu'une nouvelle
activation est mise en place. Tout nom X non local désigne dans l'activation
appelée, le même emplacement que celui qu'il désignait dans l'activation
appelante.
De nouvelles liaisons sont mises en place pour les noms locaux de la
procédure appelée ; ces noms désignent des emplacements dans le nouvel
enregistrement d'activation.

VII.6. PASSAGE DE PARAMETRES

 Lorsqu'une procédure appelle une autre, la communication entre elles se


fait par l'intermédiaire des noms non locaux ou les paramètres de la
procédure appelée.
Valeur et adresse :
 Considérer l'affectation suivante : t[i] := t[j]. L'expression t[j] représente
une valeur, alors que t[i] représente un emplacement mémoire dans
lequel la valeur t[j] sera rangée. On désigne par le terme valeur_g un

177
7. Environnements d'exécution

emplacement mémoire et par le terme valeur_d la valeur contenue dans


cet emplacement.

VII.6.1. Passage par valeur

C'est la manière la plus simple pour passer des paramètres. Les arguments
sont évalués et leurs valeur_d sont passées à la procédure appelée. Le
langage C ne connaît que le passage par valeur. On peut réaliser le passage
par valeur comme suit :
 Un paramètre formel est traité exactement comme un nom local, et les
emplacements des paramètres se trouvent donc dans le bloc d'activation
de la procédure appelée.
 L'appelant évalue les arguments et place leurs valeur_d aux
emplacements réservés aux paramètres.

VII.6.2. Passage par référence

Lors d'un passage par référence, l'appelant passe à la procédure appelée


un pointeur vers l'emplacement en mémoire de l'argument effectif.
 Si un argument est un nom ou une expression ayant une valeur_g,
alors c'est cette valeur qui est passée.
 Si l'argument est une expression sans valeur_g, cette expression est
évaluée dans un nouvel emplacement en mémoire, et c'est l'adresse de
cet emplacement qui est passée.

VII.6.3. Passage par copie-restauration


Le passage par copie-restauration est un hybride du passage par valeur et
du passage par référence. Il est explicité dans ce qui suit :
 Avant que le contrôle n'entre dans la procédure appelée, les
arguments sont évalués. Leurs valeur_d sont passées à la procédure
appelée comme dans le passage par valeur. En outre, les valeur_g des
arguments qui en ont sont déterminés avant l'appel.
 Lorsque le contrôle revient à l'appelant, la valeur_d courante de
chaque paramètre formel est recopiée dans la valeur_g de l'argument
correspondant (si une valeur_g existe), en utilisant les valeur_g
calculées à l'avance.

178
7. Environnements d'exécution

VII.6.4. Passage par nom

Le passage de paramètre par nom est défini par la règle de copie du


langage Algol et qui est donnée dans ce qui suit :
 La procédure est traitée comme s'il s'agissait d'une macro-définition,
c'est à dire son corps est substitué à l'appel dans l'appelant, les
arguments effectifs sont littéralement substitués aux paramètres
formels.
 Les noms locaux à la procédure appelée sont distingués des noms
locaux de la procédure appelante. Chaque nom local à la procédure
appelée reçoit systématiquement un nouveau nom distinct avant que
le développement de macro-définition soit fait.
 Les arguments sont placés entre parenthèses pour préserver leur
intégrité.

179
7. Environnements d'exécution

VII.7. EXERCICES

Exercice 1.
 Dessiner l'arbre d'activation du programme Pascal suivant :
program param (input,output) ;
procedure b (function h (n : integer) : integer);
var m : integer;
begin m := 3 ; writeln (h(2)) end ;

procedure c;
var m : integer;
function f(n:integer):integer;
begin
f:= m + n;
end ;
procedure r;
var m : integer;
begin
m:= 7; b(f);
end ;
begin
m:= 0; r;
end ;
begin
c;
end.

Exercice 2.
 Quel est le résultat du programme suivant, dans le cas d'un passage de
paramètres
(a) par valeur,
(b) par référence,
(c) par copie-restauration,
(d) par nom ?

180
7. Environnements d'exécution

programme principal (entrée,sortie) ;


procédure p (x,y,z);
début
y := y + 1;
z := z + x;
fin ;
début
a := 2;
b := 3;
p(a+b,a,a);
écrire(a);
fin.

Exercice 3.
 En utilisant les règles de portée du langage Pascal, déterminer pour
chaque occurrence de a et b du programme suivant, quelle déclaration s'y
applique :

program a (input,output) ;
procedure b (u,v,x,y : integer);
var a : record a,b : integer end ;
b : record b,a : integer end ;
begin
with a do begin a := u; b := v;
end ;
with b do begin a := x; b := y;
end ;
writeln (a.a, a.b, b.a, b.b)
end ;
begin
b(1,2,3,4);
end.

181
7. Environnements d'exécution

Exercice 4.
 Lorsqu'on passe une procédure en paramètre dans un langage à portée
statique, son environnement peut être passé au moyen d'un lien accès.
Donner un algorithme qui détermine ce lien.

182
8. Production de code

CHAPITRE VIII. PRODUCTION DE


CODE

VIII.1. INTRODUCTION

La phase de production de code correspondant à un programme source est


la dernière phase de la compilation. Cette phase a comme point de départ le
code intermédiaire généré par les phases précédentes (voir figure 1). Le code
généré est machine dépendant c'est à dire que le code généré est spécifique à
un type de machine (type de processeur et jeu d'instruction associé).

programme Générateur
Code programme
source de
Analyse intermédiaire cible
Code
Lexicale

Figure VIII.1. Emplacement du générateur de code.

VIII.2. MACHINE CIBLE

Le code machine étant machine dépendant, il est indispensable pour


effectuer cette phase de connaître toutes les spécificités de la machine cible
c'est à dire la machine pour laquelle est dédié le code. En particulier, il
faudra connaître le jeu d'instructions de la machine cible, le nombre de
registres, la taille des mots mémoire et les modes d'adressage.
Nous nous intéresserons dans ce chapitre à une machine fictive mais qui
s'inspire largement des processeurs Intel.
 Elle dispose de N registres numérotés : R0, R1, … , Rn-1
 Les mots ont une taille de 4 octets
 La machine est adressable par octet
 Les instructions ont un format à deux adresses :
183
8. Production de code

op source destination
qui a la signification destination ←destination op source
Exemples d'instructions :

Instruction Signification
Mov src, dst Charger la source src dans destination dst
Add src, dst Ajouter la source src à destination dst
Sub src, dst Sostraire la source src de la destination dst

Quelques modes d'adressage :

Mode d'adressage Format Adresse effective


absolu M M
registre R R
indexé c(R) c + contenu (R)
registre indexé *R contenu (R)
indirect indexé *c(R) contenu(c + contenu
(R))

Exemples d'utilisation des modes d'adressage :

Instruction Signification
Mov #1, R0 Charger la constante 1 dans registre R0
Mov 4(R0), M Charger contenu(4+contenu(R0)) dans M

Remarque :
Avant toute étape de génération de code, le coût des instructions (en
terme de temps d'exécution) doit être connu. S'il y a possibilité de choisir
entre plusieurs instructions, il faudra toujours prendre la moins coûteuse en
vue de produire le meilleur code possible.

184
8. Production de code

VIII.3. BLOCS DE BASE ET GRAPHES DE FLOT DE


CONTROLE

Avant de produire le code machine proprement dit, le code intermédiaire


(typiquement sous format à trois adresses ou en quadruplets) est parcouru
plusieurs fois. Ce code intermédiaire est tout d'abord organisé en un graphe
dit "graphe de flot de contrôle". Les nœuds de ce graphe représentent les
traitements et les arcs le flot de contrôle.
Le but de cette construction est de produire un code machine de qualité
utilisant au mieux les ressources de la machine (utiliser le plus possible des
opérandes registres, éviter les modes d'adressage complexes, …).

VIII.3.1. Blocs de Base

Chaque nœud du graphe de flot de contrôle est appelé "bloc de base". Un


bloc de base est en fait une séquence d'instructions consécutives dans
laquelle le flot de contrôle est activé au début (première instruction) et se
termine à la fin (dernière instruction) sans branchement ni arrêt.
Activité d'un nom :
Un nom dans un bloc de base est actif en un point de ce bloc si sa
valeur est utilisée après ce point dans le programme. Cette information sera
utile pour l'optimisation en terme de temps d'exécution du code généré. En
effet, une instruction calculant une variable qui ne sera plus active peut être
supprimée.
Algorithme de partitionnement :
L'algorithme suivant donne la partition d'un programme (sous forme
intermédiaire) en blocs de base :
1. Déterminer les instructions de tête de chaque bloc :
i) La première instruction du programme est une instruction de tête ;
ii) Toute instruction atteinte par branchement est une instruction de tête ;
iii) Toute instruction qui suit un branchement est une instruction de tête.
2. Pour chaque instruction de tête :
 Le bloc de base correspondant débute par cette instruction et est
augmenté des instructions qui suivent dans le programme une à une
jusqu'à atteindre une autre instruction de tête ou la fin du programme. Il
est à noter que l'instruction de tête atteinte ne fait pas partie du bloc de
base.
185
8. Production de code

Exemple :
 Considérer le programme suivant de calcul d'un produit scalaire :
Début
prod :=0;
i:=1;
Faire
Début
prod:= prod + a[i]*b[i];
i:= i + 1;
Fin
Tanque i<= 20
Fin.

 Code à trois adresses calculant le produit scalaire :


(1) prod := 0
(2) i := 1
(3) t1 := 4 * i
(4) t2 := a [t1]
(5) t3 := 4 * i
(6) t4 := b [t3]
(7) t5 := t2 * t4
(8) t6 := prod + t5
(9) prod := t6
(10) t7 := i + 1
(11) i := t7
(12) si i <= 20 aller à (3)

 Après application de l'algorithme de partition du programme précédent


en bloc de base, on obtient :
√ Bloc de base #1 constitué des instructions (1) et (2) ;
√ Bloc de base #2 constitué du reste des instructions.

186
8. Production de code

VIII.3.2. Transformations sur les blocs de Base

Le but de toute transformation sur un bloc de base est d'améliorer sa


qualité pour générer un code machine le moins coûteux (en temps
d'exécution) sans changer les expressions calculées par ce bloc de base.
On peut classifier les transformations opérant sur un bloc de base en deux
grandes catégories :
 Les transformations préservant la structure des programmes ;
 Les transformations algébriques.

VIII.3.2.1. Transformations préservant la structure des


programmes
Ces transformations sont essentiellement :
 Elimination des sous-expressions communes :
Si la valeur d'une même expression est utilisée plusieurs fois dans le
bloc de base, il est inutile de la recalculer plusieurs fois. Il suffit en effet
de la calculer une fois et de réutiliser ce résultat d'où un gain de temps
machine substantiel.
 Elimination du code inutile :
Le code qui n'est jamais atteint dans l'exécution doit être supprimé. Il en
résultera un gain en espace mémoire relatif à ce programme.
 Renommer des variables temporaires :
Utiliser le moins possible de variables temporaires pour optimiser
ensuite l'allocation des registres.
 Echange d'instructions adjacentes :
Sur les machines RISC (Reduced Instruction Set Computer), le fait
d'alterner deux instructions adjacentes permet parfois de limiter le
nombre de "bulles" dans le pipeline d'exécution.

VIII.3.2.2. Transformations algébriques


Ces transformations modifient la structure des programmes sans
modifier les résultats calculés. On peut citer comme types de
transformations algébriques :
 La simplification des expressions :
187
8. Production de code

Certaines expressions peuvent être simplifiées ou carrément éliminées.


Le but de cette opération est un gain de temps significatif. L'opération de
repliement consiste à évaluer les opérations dont les opérandes sont
connus à la compilation.
Exemples :
√ Les instructions suivantes peuvent être éliminées :
x := x + 0;
x := x * 1;
√ Les instructions suivantes :
x := 5 * 10;
y := a*(600+3000);
seront remplacées par :
x := 50;
y := a*3600;

 L'utilisation d'opérateurs moins coûteux :


Une instruction utilisant un opérateur coûteux sera remplacée par une
instruction utilisant un opérateur moins coûteux (quand cela est
possible). Les deux instructions devant effectuer le même traitement.
Exemple :
√ L'es instruction suivante :
x := y ↑ 2; (puissance deux)
sera remplacée par :
x := y * y;

VIII.3.3. Construction du graphe de flot de contrôle

Le graphe de flot de contrôle est un graphe orienté dont les sommets ou


nœuds sont les blocs de base construit précédemment. Il est obtenu par le
procédé suivant :
 Le nœud initial (ou d'entrée du programme) contient la première
instruction ;
 Un arc partant d'un bloc B1 vers un bloc B2 si B2 peut suivre
immédiatement B1 dans une exécution, c'est à dire :
√ La dernière instruction de B1 est une instruction de branchement
vers la première instruction de B2 ou,
188
8. Production de code

√ Le bloc B2 suit immédiatement le bloc B1 dans l'ordre du


programme et B1 ne se termine pas par un branchement
inconditionnel.
On peut classifier les transformations opérant sur un bloc de base en deux
grandes catégories :
Remarque :
 Dans tout ce chapitre, quand il n'est pas précisé le type de branchement,
il s'agit d'un branchement conditionnel ou inconditionnel.
Exemple :
 Le graphe de contrôle suivant correspond au programme de la section
3.1 :

prod := 0 Bloc B1
i := 1

t1 := 4 * i Bloc B2
t2 := a [t1]
t3 := 4 * i
t4 := b [t3]
t5 := t2 * t4
t6 := prod + t5
prod := t6
t7 := i + 1
i := t7
si i <= 20 aller à (3)

Définition des boucles :


 Une boucle est un ensemble de nœuds d'un graphe de flot de contrôle tel
que :
√ Tous les nœuds sont fortement connectés ;

189
8. Production de code

√ La collection de nœuds a une entrée unique.

VIII.4. UN GENERATEUR DE CODE SIMPLE

Dans cette partie nous exposerons une méthodologie pour produire du


code machine pour un bloc de base du graphe de flot de contrôle à partir
d'une représentation (code) intermédiaire sous forme de quadruplets.
Les principaux objectifs de cette phase sont :
 Générer du "bon" code i.e. un code à moindre coût ;
 Utiliser le plus possible des opérandes registres dans les instructions
générées ;
 Optimiser l'utilisation des registres car lis ne sont pas en nombre illimité.

VIII.4.1. Informations d'utilisation ultérieure

La collecte d'informations sur l'utilisation ultérieure des variables d'un


bloc de base sont indispensables à la phase d'optimisation du code produit.
Cette collecte d'informations se fait par l'application de l'algorithme suivant:
Algorithme :
i) Analyser un bloc de base de la fin vers le début ;
ii) Lorsqu'on atteint un quadruplet i : (A := B op C) ou écrit autrement
(op, B,C,A)
Faire
a) Attacher au quadruplet i les informations "actuelles" (courantes)
de la table es symboles concernant les prochaines utilisations et
l'activité des variables A, B et C ;
b) Dans la table des symboles, attacher à A l'information "pas actif"
et "pas de prochaine utilisation" ;
c) Attacher à B et C l'information "actif" et prochaine utilisation de
B et C sera faite au quadruplet N° i.
Remarques :
 Dans l'étape a) si la variable A n'est pas active alors l'instruction peut
être supprimée.
 Les étapes b) et c) ne peuvent être interverties car la variable A peut être
B ou C.

190
8. Production de code

 Pour générer le code, il faudra ensuite faire une passe sur le bloc de base
du début vers la fin.

VIII.4.2. Descripteurs de registres et d'adresses

VIII.4.2.1. Descripteurs de registres


Les descripteurs de registres gardent trace du contenu courant des
registres durant la phase de génération de code. Les descripteurs seront
consultés à chaque nouvelle allocation de registre pour produire une
instruction machine.
Initialement, le descripteur de registre indique que tous les registres de la
machine sont vides. Au cours de la génération de code, chaque registre peut
contenir zéro, un ou plusieurs noms à un instant donné.

VIII.4.2.2. Descripteurs d'adresses


Pour chaque nom dans un bloc de base, le descripteur d'adresses garde
trace de l'emplacement (ou des emplacements) où on peut trouver la valeur
courante d'un nom à l'exécution. Cet emplacement peut être un registre ou
une adresse mémoire. Les informations des descripteurs d'adresses ou de
registres peuvent être stockées dans la table des symboles.
Exemple :
 Un exemple de contenu des descripteurs de registres et d'adresses est
donné dans ce qui suit :
 Instruction d'un programme cible :
d := (a – b) + (a - c) + (a – c);
 Quadruplets correspondants :
t := a – b
u := a – c
v := t + u
d := v + u
 Tableau descriptif :
Instruction Code Produit Descripteur de Descripteur
registres d'adresses
Les registres sont
vides

191
8. Production de code

t := a - b Mov a, R0 R0 contient t t est dans R0


Sub b, R0
u := a – c Mov a, R1 R0 contient t t est dans R0
Sub c, R1 R1 contient u u est dans R1
v := t + u Add R1, R0 R0 contient v v est dans R0
R0 contient u u est dans R0
d := v + u Add R1, R0 R0 contient d d est dans R0
Mov R0, d d est dans R0 et
dans la mémoire

VIII.4.3. Algorithme de production de code

Pour produire le code machine à partir du code intermédiaire d'un bloc


de base, on fait une passe du début vers la fin sur ce bloc et on traite chaque
quadruplet.
Algorithme :
Pour chaque quadruplet A:= B op C
Faire
a) Invoquer la fonction GETREG( ) pour déterminer l'emplacement
L où le résultat B op C sera rangé (on tentera le plus possible de
trouver un emplacement registre pour L mais L peut avoir un
emplacement mémoire) ;
b) Consulter le descripteur d'adresse pour la variable B pour
sélectionner B' l'emplacement courant (si plusieurs
emplacements, choisir un emplacement registre) ;
Si B n'est pas dans L
Alors Générer Mov B', L
c) Générer l'instruction Op C', L
où C' est un des emplacements courants de C
Mise à jour du descripteur d'adresse pour A pour indiquer que A est
dans L ;
si L est un registre, mise à jour de son descripteur pour indiquer que L
contient A;

192
8. Production de code

d) Si les valeurs courantes de B et/ou C n'ont pas d'utilisation


ultérieure, ne sont pas actives à la sortie du bloc et sont dans des
registres alors mise à jour des descripteurs de registres pour
indiquer qu'après exécution de A:= B op C, ces registrent ne
contiennent plus B et/ou C.
Fonction GETREG( ) :
 Cette fonction a pour but de déterminer l'emplacement de L pour garder
la valeur de A pour l'affectation A:= B op C.
 Corps de la fonction :

i) Si le nom B est dans un registre qui ne contient pas la valeur d'autres


noms (exemple : pour X:=Y on aura deux noms dans un même
registre) et B n'est pas active et n'a pas d'utilisation ultérieure après A
:= B op C
Alors
Retourner le Registre de B pour L ;
Mise à jour du descripteur d'adresse de B: "B n'est plus dans L" ;
ii) Si i) échoue
Alors Retourner un registre vide pour L s'il y a un registre vide
disponible;
iii) Si iii) échoue
Alors
Si A a une utilisation future dans le bloc
ou Op est un opérateur qui requiert un registre
Alors
Sélectionner un registre déjà occupé R ;
Sauvegarder sa valeur en mémoire par l'instruction Mov R, M ;
Mise à jour du descripteur d'adresse pour M et retourner R ;
iv) Si A n'est pas utilisé plus loin dans le bloc
ou aucun registre occupé ne convient
Alors choisir pour L l'emplacement mémoire de A.
Remarques :
 On peut choisir comme stratégie pour sélectionner un registre dans
l'étape iii) de la fonction GETREG( ), celle qui consiste à prendre le
registre qui sera référencé le plus loin dans le bloc de base.
193
8. Production de code

VIII.4.4. Production de code pour d'autres types


d'instructions

VIII.4.4.1. Indexation et pointeurs


Les opérations d'indexation et celles portant sur les pointeurs sont
traités comme les instructions à opérateurs binaires Les exemples suivants
illustrent la génération de code pour des instructions de ce type.
Exemple 1 :

Instruction Code Code


i est dans Ri i est dans Mi

a := b[i] Mov b(Ri), R Mov Mi, R


Mov b(R), R
a[i] := b Mov b, a(Ri) Mov Mi, R
Mov b, a(R)

Exemple 2 :

Instruction Code Code


p est dans Rp p est dans Mp

a := *p Mov *Rp, a Mov Mp, R


Mov *R, R
*p := a Mov a, *Rp Mov Mp, R
Mov a, *R

Remarques :
√ Ri est un registre et Mi est un emplacement mémoire.
√ Rp est un registre et Mp est un emplacement mémoire.
√ R est le registre retourné par la fonction GETREG( ).

VIII.4.4.2. Instructions conditionnelles


La génération de code pour les instructions conditionnelles est un
processus à deux étapes. Il faut d'abord générer les instructions machines qui

194
8. Production de code

évaluent la condition et générer immédiatement après une instruction de


branchement. Le type de branchement à utiliser dépend de la condition.

Exemple :
 Considérer l'instruction de branchement suivante :
Si x < y alors aller à Label
 Son code machine sera le suivant :
CMP x, y
JL Label

CMP compare deux opérandes et positionne les bits indicateurs de la
machine ;
JL (pour Jump if Less) est un mnémonique pour brancher si plus petit.

VIII.4.5. Production de code à partir de DAG

VIII.4.5.1. DAG
Le terme DAG est l'acronyme de "Directed Acyclic Graph" (ou graphe
orienté sans cycles). Dans le contexte de la production de code, le DAG sera
utilisé pour essayer d'améliorer la qualité du code produit.
Dans cette section on s'intéressera aux DAG associés aux blocs de base.
Dans ce type de DAG, les feuilles sont étiquetées avec des identificateurs
uniques qui sont soit des noms de variables soit des constantes. Les
nœuds internes sont les symboles d'opérateurs. Les nœuds internes
portent également des étiquettes qui représentent les calculs
intermédiaires effectués dans un bloc de base.

Principe de construction d'un DAG :

 Pour construire un DAG, les quadruplets du bloc de base sont parcourus


un à un.
 A chaque quadruplet devra correspondre une partie du DAG i.e. créer un
nœud "opération" avec deux fils dans le cas d'opérations binaires ou un
nœud "opération" avec un fils dans le cas d'opérations unaires.
 Avant de créer les nœuds fils , on vérifie d'abord si un ou les deux
opérandes de l'opération considérée (correspondant au quadruplet en
195
8. Production de code

cours de traitement) ont été déjà calculés (nœuds existant déjà dans le
DAG). Si c'est le cas, on ne créera pas de nœud pour ce type d'opérande,
mais on dirigera l'arc du nœud opérateur vers le nœud déjà crée.

Exemple 1 :
 Quadruplets d'un bloc de base :
t1 := a + b
t2 := c + d
t3 := e – t2
t4 := t1 – t3
 DAG correspondant :
t4 -

t1 + -
t3

a b e
t4 +

c d

Exemple 2 :
 Quadruplets d'un bloc de base :
t1 = b * c
t2 = a + t1
t3 = d + e
t4 = t3 – e
t5 = t3 * t4
t6 = t2 – t5

196
8. Production de code

 DAG correspondant :

t6 -

t2 + t5 *

a t4
t3 + -
t1 *

b c d e

VIII.4.5.2. Heuristique d'ordonnancement


Pour produire pour un bloc de base un meilleur code que celui produit
par l'algorithme vu dans les sections précédentes, il est parfois judicieux de
réordonner les quadruplets (sans modifier les résultats obtenus) de ce bloc
en utilisant "les informations" qu'apporte un DAG. Une fois un meilleur
ordre trouvé, l'algorithme de la section devra être appliqué.
En fait, il n'existe pas de méthode "déterministe" pour réordonner les
quadruplets d'un bloc de base. Les quadruplets seront réordonnés en
appliquant une heuristique proposée par Aho et al [1]. L'idée de base de
cette heuristique est que l'évaluation d'un nœud suit (autant que possible)
l'évaluation de son fils gauche. D’après les auteurs, en général, avec ce ré-
ordre un meilleur code que celui obtenu sans réordonner les quadruplets
mais ce n'est pas toujours le cas.
L'algorithme suivant donne l'ordre inverse d'évaluation des
quadruplets pour un bloc de base :
Algorithme :
Le DAG est parcouru "en profondeur".
Initialement tous les nœuds sont non marqués ;
Tant que des nœuds internes n'ont pas été marqués
Faire
Choisir un nœud N non marqué dont tous les parents ont été marqués ;
Marquer N ;
Tant que le fils le plus à gauche de M e N n'est pas une feuille
et a tous ses parents marqués
197
8. Production de code

Faire
Marquer M;
N:= M;
Fait
Fait.

Remarque :
 L'algorithme précédent entame son analyse avec "la racine" (ou point
d'entrée) du DAG.
Exemple 1 :
√ Considérer les quadruplets suivants et supposons que la machine ne
dispose que deux registres R0 et R1.
t1 := a + b
t2 := c + d
t3 := e – t2
t4 := t1 – t3
√ L'application de l'algorithme simple de production de code donne le code
suivant :
Mov a, R0
Add b, R0
Mov c, R1
Add d, R1
Mov R0, t1
Mov e, R0
Sub R1, R0
Mov t1, R1
Sub R0, R1
Mov R1, t4
√ Application de l'heuristique sur le DAG ; le numéro de l'étoile donne
l'ordre de marquage :
*1
t4 -

t1 *2 *3
+ t3 -

198
a b e *4
t2 +

c d
8. Production de code

√ Ordre d'évaluation est l'inverse de l'ordre de marquage :


Evaluer dans cet ordre : t2 t3 t1 t4
√ Code produit après le ré-ordre :
Mov c, R0
Add d, R0
Mov e, R1
Sub R0, R1
Mov a, R0
Add b, R0
Sub R1, R0
Mov R0, t4
√ Les deux codes produits effectuent exactement le même traitement, mais
le deuxième code obtenu est sensiblement meilleur que le premier.

VIII.4.5.3. Méthode d’ordonnancement proposée


L’heuristique d’ordonnancement précédente réordonne les quadruplets de
telle sorte que l'évaluation d'un nœud suit (autant que possible) l'évaluation
de son fils gauche. Le nouvel ordre d’évaluation est l’ordre inverse du
marquage. Le but de cette heuristique est de ne pas monopoliser des
ressources registres pour certains calculs et qui ne seront utilisées qu’après
l’évaluation d’autres traitements. En d’autres termes effectuer les calculs
juste avant qu’on en ait besoin. L’exemple précédent illustre parfaitement
cette stratégie.
L’heuristique d’ordonnancement précédente peut donc donner de
meilleurs résultats si la branche gauche est moins chargée d’opérations que
le branche droite du DAG. En fait, il peut s’avérer qu’en utilisant cette
heuristique d’ordonnancement, on obtient de plus mauvais résultats que
l’algorithme de base initial. L’exemple suivant illustre notre propos.

Exemple :
 Considérer les quadruplets suivants et supposons que la machine ne
dispose que deux registres R0 et R1.
t1 := b + c
t2 := a + t1
199
8. Production de code

t3 := d + e
t4 := t2 + t3
√ L'application de l'algorithme simple de production de code donne le code
suivant :
Mov b, R0
Add c, R0
Mov a, R1
Add R0, R1
Mov d, R0
Add e, R0
Add R0, R1
√ Application de l'heuristique sur le DAG ; le numéro de l'étoile donne
l'ordre de marquage :

*1
t4 -
*2
*4
t2
+ t3 -

*3
a t1 + d e

b c

√ Ordre d'évaluation est l'inverse de l'ordre de marquage :


Evaluer dans cet ordre : t3 t1 t2 t4
√ Code produit après le ré-ordre :
Mov d, R0
Add e, R0
Mov b, R1

200
8. Production de code

Add c, R1
Mov R0, t3
Mov a, R0
Add R1, R0
Add t3, R0
Le code produit après application de l’heuristique d’ordonnancement est
plus mauvais que le code produit par l’algorithme simple.
Après ce constat, il ressort que l’heuristique proposée n’est pas fiable et
privilégie systématiquement l’évaluation d’un nœud juste après l’ évaluation
de son fis gauche. Nous proposons l’algorithme d’ordonnancement suivant
dont l’idée est que l’évaluation d’un nœud suit l’évaluation de la branche
fille la moins chargée en terme d’opérations. Un traitement supplémentaire
est donc nécessaire pour évaluer en chaque nœud le nombre d’opérations de
sa descendance.
L'algorithme suivant donne l'ordre inverse d'évaluation des
quadruplets pour un bloc de base. L'algorithme entame son analyse avec "la
racine" (ou point d'entrée) du DAG
Algorithme :
Initialement tous les nœuds sont non marqués ;
Tant que des nœuds internes n'ont pas été marqués
Faire
Choisir un nœud N  marqué dont tous les parents ont été marqués ;
Marquer (N) ;
Fait.

Marquer(N)
{ Marquage de N par un compteur ; incrémenter compteur ;
ng=nombre opérations branche gauche ;
nd=nombre opérations branche droite ;
Si (ng<nd) et (fils gauche G  feuille et tous ses parents marqués)
Alors Marquer (G) ;
Si (fils droit D  feuille et tous ses parents marqués)
Alors Marquer (D) ;

201
8. Production de code

FinSi ;
Sinon
Si (ng≥nd) et (fils droit D  feuille et tous ses parents marqués)
Alors Marquer (D) ;
Si (fils gauche G  feuille et tous ses parents marqués)
Alors Marquer (G) ;
FinSi ;

Remarque :
 L'algorithme précédent donne de meilleurs résultats que l’heuristique
donnée précédemment.

Exemple :
 Considérer les quadruplets suivants et supposons que la machine ne
dispose que 3 registres R0,R1 et R2.

t1 := b + c
t2 := a + t1
t3 := d + e
t4 := t2 + t3
t5 := f + g
t6 := j – k
t7 := i + t6
t8 := e – t7
t9 := t5 – t8
t10 := t4 + t9

i. Code obtenu par l’application de l’algorithme simple :


MOV b, R0
ADD c, R0
MOV a, R1
ADD R0, R1
MOV d, R2
202
8. Production de code

ADD e, R2
ADD R2, R1
MOV f, R0
ADD g, R0
MOV j, 2
SUB k, R2
MOV R1, t4
MOV i, R1
ADD R2, R1
MOV e, R2
SUB R1, R2
SUB R2, R0
MOV t4, R1
ADD R0, R1

203
8. Production de code

ii. Production du code à partir du DAG:


a. Construction du DAG
*1
t10 +

*2
*6
t4 -
+ t9
*3
*5 *7 *8
t2
+a t3 + t5 + t8 -

*4 *9
t7
a t1 + d e f g +a

*1

b c i t6 -

j k
b. Ordonnancement des quadruplets
L’ordre d'évaluation des quadruplets est l'inverse de l'ordre de
marquage (* suivie de la chronologie du marquage). Dans l’exemple
considéré, l’ordre d’évaluation est : t6 t7 t8 t5 t9 t3 t1 t2 t4 t10
c. Production de code :
MOV j, R0
SUB k, R0
MOV i, R1
ADD R0, R1
MOV e, R2
SUB R1, R2
MOV f, R0
ADD g, R0
SUB R2, R0
MOV d, R1
ADD e, R1
MOV b, R2
ADD c, R2
MOV R0, t9
MOV a, R0
ADD R2, R0
ADD R1, R0

204
8. Production de code

ADD t9, R0
iii. Production du code à partir de la méthode proposée :
a. Ordonnancement des quadruplets
Le DAG considéré est le même que celui construit précédemment. Le
marquage du DAG est fait en appliquant la méthode proposée.
L’ordre d'évaluation des quadruplets est: t6 t7 t8 t5 t9 t1 t2 t3 t4 t10
b. Production de code
MOV j, R0
SUB k, R0
MOV i, R1
ADD R0, R1
MOV e, R2
SUB R1, R2
MOV f, R0
ADD g, R0
SUB R0, R2
MOV b, R0
ADD c, R0
MOV a, R1
ADD R0, R1
MOV d, R0
ADD e, R0
ADD R0, R1
ADD R2, R1
iv. Comparaison des codes générés :
On constate que l’application de l’heuristique d’ordonnancement
proposée par Aho et al [1] avant la production de code améliore dans ce
cas le code produit par rapport à l’algorithme simple. L’application de
l’ordonnancement proposé permet d’améliorer d’avantage le code
produit.

VIII.4.6. Assignation globale de registres


Dans ce qui suit, sera abordé la problématique d'assignation des registres
lors de la production de code dans le cas d'un graphe de flot de contrôle
contenant des boucles. Les variables "très utilisées" dans les boucles seront
privilégiées lors de l'affectation des registres.

VIII.4.6.1. Vivacité des variables


 Pour un nom A en un point p (quadruplet), si la valeur de A en p peut
être utilisé le long d'un chemin du graphe de flot de contrôle en partant

205
8. Production de code

de p alors A est vivante en p ; autrement la variable A n'est pas


vivante.

VIII.4.6.2. Définitions de "l'usage" des variables


 Soit B un bloc de base, les ensembles suivants serviront à quantifier
l'utilisation des variables du graphe du flot de contrôle.
 IN[B] : ensemble des noms vivants au point immédiatement
avant d'entrer en B.
 OUT[B] : ensemble des noms vivants au point immédiatement
après la sortie de B.
 DEF[B] : ensemble des noms assignés avant toute utilisation
de ce nom en B.
 USE[B] : ensemble des noms utilisés en B avant toute
définition.
 Relations entre ces ensembles :
 IN[B] = OUT[B] - DEF[B] ∪ USE[B]
 OUT[B] = ∪ IN[S] où S est un successeur de B
 Exemple :

bcdf
a := b + c
d := d - b Bloc B1
e := a + f
acdef
acde acdf
f := a - d Bloc B2 Bloc b := d +f
B3 e := a - c
bcdef
cdef
Bloc B4 b := d + c b d e f "vivantes"
bcdef

bcdef
"vivantes"
 a est vivante en sortie de B1 mais n'est plus vivante en sortie de
B2, B3 et B4.

206
8. Production de code

VIII.4.6.3. Compteur d'usage


 Le compteur d'usage d'une variable a dans une boucle L est donné par la
formule suivante :
∑ (USE(a dans B) + 2*LIVE(a dans B))
Blocs B
Dans L
 USE[a,B] : Nombre de fois où a est utilisé dans B avant toute
définition de a.
 LIVE[a,B] = 1 si a est vivante en sortie de B et a est assignée
dans B ; (= 0 sinon)

VIII.4.6.4. Stratégie d'allocation


 Affecter des registres aux variables ayant des compteurs d'usage les plus
élevés.
Application
USE(a,B1)=0 LIVE(a,B1)=1
USE(a,B2)=1 LIVE(a,B2)=0
USE(a,B3)=1 LIVE(a,B3)=0
USE(a,B4)=0 LIVE(a,B4)=0

USE(b,B1)=2 LIVE(b,B1)=0
USE(b,B2)=0 LIVE(b,B2)=0
USE(b,B3)=0 LIVE(b,B3)=1
USE(b,B4)=0 LIVE(b,B4)=1

USE(c,B1)=1 LIVE(c,B1)=0
USE(c,B2)=0 LIVE(c,B2)=0
USE(c,B3)=1 LIVE(c,B3)=0
USE(c,B4)=1 LIVE(c,B4)=0

USE(d,B1)=1 LIVE(d,B1)=1
USE(d,B2)=1 LIVE(d,B2)=0
USE(d,B3)=1 LIVE(d,B3)=0
USE(d,B4)=1 LIVE(d,B4)=0

USE(e,B1)=0 LIVE(e,B1)=1
USE(e,B2)=0 LIVE(e,B2)=0
USE(e,B3)=0 LIVE(e,B3)=1
USE(e,B4)=0 LIVE(e,B4)=0
207
8. Production de code

USE(f,B1)=1 LIVE(f,B1)=0
USE(f,B2)=0 LIVE(f,B2)=1
USE(f,B3)=1 LIVE(f,B3)=0
USE(f,B4)=0 LIVE(f,B4)=0

 Compteurs d'usage des variables de l'exemple précédent :


Variable a b c d e f
Compteur d'usage 4 6 3 6 4 4
 Donner le code produit pour le graphe de flot de contrôle précédent
sachant que les registres R0, R1 et R2 seront assignées aux variables (a,
b et d) ayant un compteur d'usage élevé. La variable a été arbitrairement
choisi par rapport à e t f.

 Code machine :
MOV b, R1
MOV d, R2

MOV R1, R0
ADD c, R0
SUB R1, R2
MOV R0, R3
ADD f, R3
MOV R3, e

SUB R2, R0 MOV R2, R1


MOV R0, f ADD f, R1
MOV R0, R3
SUB c, R3
MOV R3, e

MOV R2, R1 MOV R1, b


ADD c, R1 MOV R2, d

MOV R1, b
MOV R2, d

208
8. Production de code

VIII.5. EXERCICES

Exercice 1.
 En utilisant l'algorithme de la section 4, produire le code pour les
instructions C suivantes :
i) x = a +b * c;
ii) x = a / (b + c) – d * (e + f);
Nb. On suppose qu'on dispose des instructions MUL et DIV pour effectuer
la multiplication et la division.

Exercice 2.
 Produire le code relatif au programme C suivant :
main()
{ int i;
int a[10];
i = 1;
while (i <= 10)
{
a[i] = 0;
i = i + 1;
}
}

Exercice 3.
 Produire le code relatif au programme Pascal suivant :
Begin
A: array [1..10, 1..5, 1..2] of integer;
B: array [1..10, 1..5] of integer;
i:= 1; j:= 1;
While j ≤ 5
Begin
alpha:= 3,14; n:= E;
gamma:= alpha*2 + 3*j;
A[i,j,2]:= B[i,j] + gamma;
j:= j + 1;
end;
end.
209
8. Production de code

Exercice 3.
a) Expliquer comment construire le DAG correspondant à une expression
arithmétique.
b) Construire le DAG pour l'expression arithmétique suivante :
t = (a+b*c) +d*e-b*c +(a+b*c)/(b*c)+d*e
c) Générer à partir du DAG, les quadruplets permettant d'évaluer
l'expression précédente (les sous expressions communes ne seront pas
réévaluées) sans optimiser le nombre de temporaires.
d) Générer le code machine correspondant à ces quadruplets en appliquant
l'algorithme vu en cours. Trois registres R0, R1 et R2 sont disponibles.
On dispose des codes opérations suivants : MOV, ADD, SUB, DIV et
MUL. Chaque instruction arithmétique a la forme suivante :
OP src, dest
dont la signification est :
dest ← dest OP src
e) Appliquer l'heuristique sur le DAG pour trouver un autre ordre
d'évaluation des quadruplets. Générer alors le code machine
correspondant. Comparer les codes machines obtenus ; commenter.

Exercice 4.
 Considérer le code intermédiaire suivant :

t1 = b * c
t2 = a + t1
t3 = d + e
t4 = t3 – e
t5 = t3 * t4
t6 = t1 – t5
a) Générer le code machine correspondant à ces quadruplets en appliquant
l'algorithme vu en cours. Trois registres R0, R1 et R2 sont disponibles.
On dispose des codes opérations suivants : MOV, ADD, SUB, DIV et
MUL. Chaque instruction arithmétique a la forme suivante :
OP src, dest

210
8. Production de code

dont la signification est :


dest ← dest OP src
b) Concernant la stratégie du choix du registre à utiliser quand il n y a plus
de registres disponibles, la stratégie utilisée est-elle la meilleure ?
Justifiez votre réponse.
c) Construire le DAG correspondant au code intermédiaire précédent.
d) Appliquer l'heuristique sur le DAG pour trouver un autre ordre
d'évaluation des quadruplets.
e) Générer alors le code machine correspondant. Comparer les codes
machines obtenus ; commenter.

211
BIBLIOGRAPHIE
[1] Titre : Compilateurs : Principes, techniques et outils
Auteurs : Aho, Ullman & Sethi.
Edition : DUNOD 2000.
[2] Titre : Principles of compiler design
Auteurs : Aho & Ullman.
Edition : Addison Wesley, 1977.
[3] Titre : Yacc: Yet Another Compiler-Compiler
Auteur : Stephen C. Johnson
Computing Science Technical Report No. 32, Bell Laboratories, Murray
Hill, NJ 07974.
[4] Titre : Modern Compiler Design.
Auteur : D. Grune
Edition : John Wiley & Sons, 2000.
ISBN : 0 471 97697 0.
[5] Titre : Introduction to Automata Theory, Languages and
Computation
Auteurs : J.E. Hopcroft & J.D. Ullman.
Edition : Addison Wesley, 1979.
[6] Titre : Compiler Construction : Principles and Practice
Auteur : K.C. Louden
Edition : Course Technology, 1997.
ISBN : 0 534 93972 4.
[7] Titre : Réaliser un compilateur, les outils Lex et YACC
Auteur : N. Silverio.
Edition : Eyrolles, 1994.
[8] Titre : Lex & Yacc
Auteur : J. Levine, T. Mason, D. Brown
Edition : O(Reilly), 1992.
ISBN : 1 56592 000 7.
[9] Titre : Generating Parsers with JavaCC
Auteur : Tom Copeland
Edition : Centennial Books, Alexandria, VA, 2007.
ISBN : 0-9762214-3-8
ANNEXE A. Classification des Grammaires
Le linguiste Noam Chomsky a classifié les grammaires en quatre classes
données par le tableau suivant :
Grammaire Forme des productions : α→β
Type 0 α,β chaînes arbitraires de symboles de la grammaire avec α≠ε
Nom associé
Grammaires Non Restreintes (Unrestricted Grammars)
Outil formel associé
Machine de Turing
Grammaire Forme des productions : α→β
Type 1 avec |α| < |β|
Nom associé
Grammaires Contextuelles (Context Sensitive Grammars)
Outil formel associé
Automate Linéaire Borné
Grammaire Forme des productions : A→α
Type 2 avec A∈N et α∈ (T∪N)*
Nom associé
Grammaires à Contexte Libre (Context Free Grammars)
Outil formel associé
Automate à Pile
Grammaire Forme des productions
Type 3 Grammaires linéaire droite (resp. gauche)
A→ωB | ω ou A→Bω | ω avec (A,B)∈ N2, ω∈T*
Grammaires régulière à droite (resp. à gauche) normalisée
A→aB | a ou A→Ba | a avec (A,B)∈ N2, a∈T
Nom associé
Grammaires Régulières (Regular Grammars)
Outil formel associé
Automate d’Etats Finis
ANNEXE B. Formes Normales des
Grammaires

B1. Forme Normale de Chomsky


B1.1. Définition
 Une grammaire Context-Free est dite sous forme normale de Chomsky
si et seulement si toutes les productions de la grammaire sont sous la
forme :
A→BC ou A→a avec (A,B,C)∈ N3, a∈T
B1.2. Transformation d’une grammaire Context-Free en une
Grammaire sous forme normale de Chomsky
 L’algorithme suivant transforme toute grammaire Context-Free ne
contenant ni ε-production, ni symbole inutile ni production unitaire en
une grammaire sous forme normale de Chomsky.
Algorithme :
i. Les productions sous forme A→a avec A∈ N, a∈T sont déjà sous une
forme acceptable.
ii. Pour chaque production de la forme A→X1X2…Xm avec m≥2
Faire Si Xi est un terminal a
Alors introduire un nouveau non terminal Ca
Introduire la production Ca→a
Remplacer Xi par Ca
iii. Pour chaque production A→B1B2…Bm avec m≥3 (B1,B2,…,Bm)∈ Nm
Faire Créer non-terminaux D1, D2, Dm-2
Remplacer A→B1B2…Bm
Par A→B1D1, D1→B2D2, ... Dm-3→Bm-2Dm-2, Dm-2→Bm-1Bm
B2. Forme Normale de Greibach
B.2.1. Définition
 Une grammaire Context-Free est dite sous forme normale de Greibach
si et seulement si si chacun des membres droits de ses règles commence
par un terminal.:
A→aγ avec A∈ N, a∈T, γ∈(N-{S})*
B1.2. Transformation d’une grammaire Context-Free en une
Grammaire sous forme normale de Greibach
 L’algorithme suivant transforme toute grammaire Context-Free sous
forme normale de Chomsky en une grammaire sous forme normale de
Greibach.
Algorithme :
Etape 1 :
1. Ordonner les non-terminaux A1,A2,…,An (exemple:si A→Bγ alors A<B)
2. Pour i=1 à n
Faire
Pour j=1 à (i-1)
Faire Remplacer chaque production de la forme Ai→Ajγ par les
productions Ai→ δ1γ |δ2γ |…|δpγ où les Aj productions sont
Aj→ δ1 |δ2 |…|δp
Fait ;
Remplacer Ai→ Aiα1| Aiα2|…|Aiαm|β1 |β2 |…|βk
Par Ai→ β1A’i |β2A’i |…|βkA’i|β1 |β2 |…|βk
A’i→ α1A’i |α2A’i |…|αmA’i|α1| α2|…|αm
Fait
Remarque :
A la fin de l’étape 1, on aura uniquement des productions de la forme :
 Ai→Ajγ avec j > i, a∈T, γ∈(N∪{A’1,A’2,…,A’n})*
 Ai→aγ
 A’i→γ
avec j > i, a∈T, γ∈(N∪{A’1,A’2,…,A’n})*
Etape 2 :
 Nota bene : Les productions dont An est membre gauche de production
débutent par un terminal
Pour i=n-1 retour à 1
Faire
Pour chaque production Ai→Ajγ (j>i)
Faire Substituer dans cette production Aj par ses membres droits de
production i.e. si Aj→ θ1 |θ2 |…|θs alors Ai→Ajγ sera remplacé
par Ai→ θ1γ|θ2γ |…|θsγ
Fait ;
Fait ;
Remarque :
A la fin de l’étape 2, tous les membres droits des Ai productions débutent
par un terminal.

Etape 3 :
Pour chaque production A’k→Aiγ
Faire Remplacer dans cette production Ai par ses membres droits de
production
Fait

Vous aimerez peut-être aussi