Cours 1
Cours 1
Cours 1
2020-2021
Compilation, L3 Informatique , Dr Sari&Souici, Université d’Annaba, 2020/2021 2
DESCRIPTIF OFFICIEL
Objectifs
1. Compréhension du cheminement d'un programme (texte) source vers un programme (code).
2. Etude des étapes du processus de compilation d’un langage évolué.
3. Etude de méthodes et techniques utilisées en analyse lexicale, syntaxique et sémantique.
4. Familiarisation, en TP, avec des outils de génération d’analyseurs lexicaux et syntaxiques (LEX et YACC).
Contenu
1 Introduction à la compilation
Les différentes étapes de la compilation
Compilation, interprétation, traduction
2 Analyse Lexicale
Expressions régulières
Grammaires
Automates d’états finis
Un exemple de générateur d’analyseurs lexicaux : LEX
3 Analyse Syntaxique
Définitions : grammaire syntaxique, récursivité gauche, factorisation d’une grammaire, grammaire -libre
Calcul des ensembles des débuts et suivants
Méthodes d’analyse descendante : la descente récursive, LL(1)
Méthodes d’analyse ascendante : SLR(1), LR(1), LALR(1) (méthode des items)
Un exemple de générateur d’analyseurs syntaxiques : YACC
5 Formes intermédiaires
Forme postfixée
Quadruplés
Triplés directs et indirects
Arbre abstrait
Références Bibliographiques
Ouvrages existants au niveau de la bibliothèque de l’université, la référence 1 est vivement recommandée
1. Aho A., Sethi R., Ullman J., "Compilateurs : Principes, techniques et outils", Inter-éditions, 1991 et Dunod,
2000
2. Drias H., "Compilation: Cours et exercices", OPU, 1993
3. Wilhem R., Maurer D., "Les compilateurs: Théorie, construction, génération", Masson, 1994
1.1 Définition
Un compilateur est un programme qui a comme entrée un code source écrit en langage de haut niveau
(langage évolué) est produit comme sortie un code cible en langage de bas niveau (langage d’assemblage ou
langage machine).
La traduction ne peut être effectué que si le code source est correct car, s’il y a des erreurs, le rôle du
compilateur se limitera à produire en sortie des messages d’erreurs (voir figure 1.1).
Messages d’erreurs
Un compilateur est donc un traducteur de langage évolué qu’on ne doit pas confondre avec un interpréteur qui
est un autre type de traducteur. En effet, au lieu de produire un programme cible comme dans le cas d’un
compilateur, un interpréteur exécute lui-même au fur et à mesure les opérations spécifiées par le programme
source. Il analyse une instruction après l’autre puis l’exécute immédiatement. A l’inverse d’un compilateur, il
travaille simultanément sur le programme et sur les données. L’interpréteur doit être présent sur le système à
chaque fois que le programme est exécuté, ce qui n’est pas le cas avec un compilateur. Généralement, les
interpréteurs sont assez petits, mais le programme est plus lent qu’avec un langage compilé. Un autre
inconvénient des interpréteurs est qu’on ne peut pas cacher le code, et donc garder des secrets de fabrication :
toute personne ayant accès au programme peut le consulter et le modifier comme elle le veut. Par contre, les
langages interprétés sont souvent plus simples à utiliser et tolèrent plus d’erreurs de codage que les langages
compilés. Des exemples de langages interprétés sont : BASIC, scheme, CaML, Tcl, LISP, Perl, Prolog
Il existe des langages qui sont à mi-chemin de l’interprétation et de la compilation. On les appelle langages P-
code ou langages intermédiaires. Le code source est traduit (compilé) dans une forme binaire compacte (du
pseudo-code ou p-code) qui n’est pas encore du code machine. Lorsqu’on exécute le programme, ce P-code est
interprété. Par exemple en Java, le programme source est compilé pour obtenir un fichier (.class) « byte code »
qui sera interprété par une machine virtuelle. Un autre langage p-code : Python.
Les interpréteurs de p-code peuvent être relativement petits et rapides, si bien que le p-code peut s’exécuter
presque aussi rapidement que du binaire compilé. En outre les langages p-code peuvent garder la flexibilité et
la puissance des langages interprétés.
Compilateur
Code
Analyseur Lexical
Source
Gestionnaire des
Unités lexicales
erreurs
Messages
Analyseur Syntaxique
d’erreurs
Arbre Syntaxique
Analyseur Sémantique
Arbre Syntaxique
Décoré
Générateur de Code
Intermédiaire
Optimiseur de Code
Code Intermédiaire
Optimisé
Générateur de Code
Code
Cible
a Identificateur de variable
:= Symbole d’affectation
b Identificateur de variable
+ Opérateur d’addition
2 Valeur entière
* Opérateur de multiplication
c Identificateur de variable
; Séparateur
L’analyseur a aussi comme rôle l’élimination des informations inutiles pour l’obtention du code cible, le stockage
des identificateurs dans la table des symboles et la transmission d’une entrée à l’analyseur syntaxique.
Concernant les informations inutiles, il s’agit généralement du caractère espace et des commentaires.
:= := :=
a + a + a Valeur de
b+ 2*c
:=
a +
b *
Conversion
entier-réel c
2
Figure 1.4. Enrichissement de l’arbre syntaxique lors de la phase d’analyse sémantique
Unités lexicales
Code Source (Fichier de
(Fichier symboles)
texte) …,
…
Analyseur Ident(a), SymAffect,
Analyseur
a := b+2*c ; lexical Ident (b), SymAdd, syntaxique
… NbEnt(2), SymMul
Ident(c), SymPtVirg,
…
(on peut ajouter un nouvel état initial d’où partent un ensemble d’arcs étiquetés ).
Etape 4 : Déterminisation ou transformation de l’AFN obtenu en AFD
Rendre l’automate de l’étape 3 déterministe.
Etape 5 : Minimisation de l’AFD.
Minimiser l’automate obtenu à l’étape 4.
Etape 6 : Implémentation de l’AFD minimisé.
Implémenter l’automate obtenu à l’étape 5 en le simulant à partir de sa table de transition.
début a b b
0 1 2 3
a a b b
0 0 1 2 3
a a b b
0 0 0 0 0
début a
2. Pour r = a N(a) est représenté par: i
début
3. Pour N(r s) on a : i N(r) N(s)
N(r)
ε ε
4. Pour N(r|s) on a :
début
i
ε ε
N(s)
début ε N(r) ε
i
Exemple
L’AFN qui reconnaît l’expression régulière (a|b)*abb selon la méthode de construction de Thompson est donné
dans la figure 2.6 :
ε (a | b)
2 a 3
ε ε
début ε ε a b b
0 1 6 7 8 9 10
ε b ε
4 5
ε
Figure 2.6. Automate fini non déterministe N pour accepter (a|b)*abb
Compilation, L3 Informatique , Dr Sari&Souici, Université d’Annaba, 2020/2021 15
2.5.4 Déterminisation d’un AFN (étape 4)
La déterminisation consiste à transformer un AFN en un AFD équivalent. Elle peut être effectuée par différentes
méthodes, nous en présentons trois dans ce qui suit.
Exemple
Appliquons l’algorithme précédent sur l’AFN dont e0 =0 et F = {2, 3} et ayant la table de transition suivante :
Symboles
Etats a b
0 {0, 2} {1}
1 {3} {0, 2}
2 {3, 4} {2}
3 {2} {1}
4 / {3}
Après application de l’algorithme, nous obtenons la table de transition de l’AFD suivante :
Symboles
Etats a b
{0} {0, 2} {1}
{0, 2} {0, 2, 3, 4} {1, 2}
{1} {3} {0, 2}
{0, 2, 3, 4} {0, 2, 3, 4} {1, 2, 3}
{1, 2} {3, 4} {0, 2}
{3} {2} {1}
{1, 2, 3} {2, 3, 4} {0, 1, 2}
{3, 4} {2} {1, 3}
{2} {3, 4} {2}
{2, 3, 4} {2, 3, 4} {1, 2, 3}
{0, 1, 2} {0, 2, 3, 4} {0, 1, 2}
{1, 3} {2, 3} {0, 1, 2}
{2, 3} {2, 3, 4} {1, 2}
Après renumérotation la table de l’AFD devient :
Symboles
Etats a b
A B C
B D E
C F B
D D G
E H B
F I C
G J K
H I L
Compilation, L3 Informatique , Dr Sari&Souici, Université d’Annaba, 2020/2021 16
I H I
J J G
K D K
L M K
M J E
ε
i j ij
2. Si l’état i mène à l’état j et l’état k par des transitions identiques (même étiquette), alors les états j et k
appartiennent à la même classe.
a j
a
i i jk
a
k
3. Une classe d’état est finale dans l’AFD si elle contient au moins un état final de l’AFN.
Ainsi, l’AFD équivalent à un AFN original possède des états qui sont des classes (regroupements) d’états de
l’automate original. Les nœuds de l’AFD correspondent à des sous ensembles de nœuds de l’AFN qui ne sont
pas forcement disjoints.
Si l’AFN à n nœuds, l’AFD peut en avoir 2n. Donc l’AFD peut être beaucoup plus volumineux que l’AFN
équivalent, cependant, en pratique le nombre d’états est souvent plus petit que 2 n.
Exemple
Considérons l’automate non déterministe de la figure 2.7, qui reconnaît l’ensemble des chaînes {a, aa, ab, aba,
abb}.
début ε a
0 1 4
a a
b a|b
2 3
a a|b
début ε ε ε
1 2 3 4
ε a
5 ε
ε
6 7
8
Algorithme ConstructionSousEnsembles ;
Début
ε-fermeture(e0) est l’unique état de Détats et il est non marqué;
Tantque il existe un état non marqué dans Détats Faire
Début
marquer T ;
Pour chaque symbole d’entrée a faire
Début
U ε-fermeture(Transiter(T,a)) ;
Si U Détats Alors Ajouter U comme nœud non marqué à Détats
DTrans[T, a] U ;
Fin ;
Fin ;
Fin ;
Exemple : Application de l’algorithme de construction de sous ensemble sur l’AFN obtenu dans la figure 2.6.
ε-fermeture(0) = {0, 1, 2, 4, 7} = A
Pour T=A
Marquer A
ε-fermeture(Transiter(A, a)) = ε-fermeture({3, 8}) = {3, 6, 1, 2, 4, 7, 8} = {1, 2, 3, 4, 6, 7, 8} = B
DTran [A, a] B
ε-fermeture(Transiter(A, b)) = ε-fermeture({5}) = {5, 6, 1, 2, 4, 7} = {1, 2, 4, 5, 6, 7} = C
DTran [A, b] C
On continue à appliquer l’algorithme avec les états actuellement non marqués B et C.
Pour T=B
Marquer B
ε-fermeture(Transiter(B, a)) = ε-fermeture({3, 8}) = B
DTran [B, a] B
ε-fermeture(Transiter(B, b)) = ε-fermeture({5, 9}) = {5, 6, 1, 2, 4, 7, 9} = {1, 2, 4, 5, 6, 7, 9} = D
b
C b
début
a a
A
b b
B D E
a a
1- Commencer avec la ε-fermeture de l’état initial (elle représente le nouvel état initial) ;
2- Rajouter dans la table de transition toutes les ε-fermetures des nouveaux états produits
avec leurs transitions ;
3- Recommencer l’étape 2 jusqu’à ce qu’il n’y ait plus de nouvel état ;
4- Tous les ε-fermetures contenant au moins un état final du premier automate
deviennent finaux ;
5- Renuméroter les états en tant qu’états simples.
Exemple : Application de cette formulation de l’algorithme de construction de sous ensemble sur l’AFN obtenu
dans la figure 2.6.
La table de transition de l’AFD obtenu est donnée par :
Symboles
Etats a b
0,1,2,4,7 1,2,3,4,6,7,8 1,2,4,5,6, 7
1,2,3,4,6,7,8 1,2,3,4,6,7,8 1,2,4,5,6,7,9
1,2,4,5,6, 7 1,2,3,4,6,7,8 1,2,4,5,6, 7
1,2,4,5,6,7,9 1,2,3,4,6,7,8 1,2,4,5,6,7,10
1,2,4,5,6,7,10 1,2,3,4,6,7,8 1,2,4,5,6, 7
Après renumérotation des états, nous obtenons la table suivante qui est identique à celle obtenue avec la
première formulation de l’algorithme de construction de sous ensembles (voir juste avant la figure 2.11) :
Symboles
Etats a b
A B C
B B D
C B C
D B E
E B C
On dit que deux états sont équivalents si ces états permettent d’atteindre un état final à travers la
même chaîne. En d’autres termes, tous les suffixes reconnus à partir de ses états sont exactement les
mêmes.
Par exemple, la figure 2.12 montre la minimisation d’un automate contenant deux états équivalents
(états 3 et 4).
a b a b
a 1 3 a 1 34
début début
0 5 0 5
b
b b b
2 4 b 2 b
1. Définir deux classes, C1 contenant les états finaux et C2 les états non finaux.
2. S’il existe un symbole a et deux états e 1 et e2 d’une même classe tels que Transiter(e 1, a) et
Transiter(e2, a) n’appartiennent pas à la même classe (d’arrivée), alors créer une nouvelle classe
et séparer e1 et e2. On laisse dans la même classe tous les états qui donnent un état d’arrivée
dans la même classe.
3. Recommencer l’étape 2 jusqu’à ce qu’il n’y ait plus de classes à séparer.
4. Chaque classe restante forme un état du nouvel automate.
Générateur
Définition Analyseur
d’Analyseur lexical
des unités
lexicales
lexicaux (en langage C)
(LEX)
Le générateur LEX permet de générer un analyseur lexical en langage C à partir des spécifications des unités
lexicales exprimées en langage LEX. Le noyau de LEX permet de produire l’automate d’état fini correspondant à
la reconnaissance de l’ensemble des unités lexicales spécifiées. Ainsi, LEX construit une table de transition pour
un automate à états finis à partir des modèles d’expressions régulières de la spécification en langage LEX.
L’analyseur lexical lui même consiste en un simulateur d’automate à états finis (en langage C), qui utilise la
table de transition obtenue pour rechercher les unités lexicales dans le texte en entrée. La spécification des
unités lexicales est effectuée en utilisant le langage LEX, en créant un programme lex.l constitué de trois
parties :
Déclarations
%%
Règles de traduction
%%
Procédures auxiliaires
La première partie contient des déclarations de variables, de constantes et des définitions régulières utilisées
comme composantes des expressions régulières qui apparaissent dans les règles de traduction.
Les règles de traduction sont des instructions (en langage C) de la forme m i {actioni} où chaque mi est une
expression régulière et chaque actioni est un fragment de programme qui décrit quelle action l’analyseur lexical
devrait réaliser quand une unité lexicale concorde avec le modèle m i.
Les procédures auxiliaires sont toutes les procédures qui pourraient être utiles dans les actions.
L’analyseur syntaxique reconnaît la structure syntaxique d’un programme source et produit une représentation
qui est généralement sous forme d’un arbre syntaxique. Ce dernier peut être décoré par des informations
sémantiques puis utilisé pour produire un programme cible.
Pour les langages simples, l’analyseur syntaxique peut ne pas livrer une représentation explicite de la structure
syntaxique du programme, mais remplit le rôle d’un module principal qui appelle des procédures d’analyse
lexicale, d’analyse sémantique et de génération de code.
Ainsi, les principales fonctions d’un analyseur syntaxique peuvent être résumées comme suit :
1. L’analyse de la chaîne des unités lexicales délivrées par l’analyseur lexical, pour vérifier si cette chaîne
peut être engendrée par la grammaire du langage considéré.
3. La construction éventuelle d’une représentation interne qui sera utilisée par les phases ultérieures.
2. Méthodes descendantes : Ces méthodes sont dites aussi Top-Down car elles construisent des arbres
d’analyse de haut en bas (de la racine aux feuilles).
3. Méthodes ascendantes : Ces méthodes sont dites aussi Bottom-up car elles construisent des arbres
d’analyse de bas en haut (remontent des feuilles à la racine).
Exemple d’analyse
SB
B R | (B)
RE=E
E a | b | (E+E)
VT = {a, b, (, ), =, +}
VN = {S, B, R, E}
SB
(B)
(R)
(E=E)
(a=E)
(a=(E+E))
(a=(b+E))
(a=(b+a))
L’analyse de la chaîne (a= (b + a)) par une approche ascendante débute au niveau de la chaîne elle-même
(lire de bas en haut) et remplace, à chaque étape, une partie de la chaîne par un non-terminal, par
réductions successives, comme suit (si on réduit à partir de la gauche) :
S
B
(B)
(R)
(E=E)
(E=(E+E))
(E=(E+a))
(E=(b+a))
(a=(b+a))
Les méthodes ascendantes et descendantes sont utilisées fréquemment pour la mise en oeuvre de
compilateurs. Dans les deux cas, l’entrée de l’analyseur syntaxique est parcourue de la gauche vers la droite, un
symbole à la fois. Les méthodes ascendantes et descendantes les plus efficaces fonctionnent uniquement avec
des sous-classes de grammaires telles que les grammaires LL et LR (qui seront étudiées aux chapitres 4 et 5)
qui sont suffisamment expressives pour décrire la majorité des structures syntaxiques des langages de
programmation.
Les analyseurs syntaxiques mis en œuvre manuellement utilisent généralement des méthodes descendantes et
des grammaires LL. C’est le cas, par exemple, pour l’analyse prédictive et la descente récursive.
Les méthodes ascendantes sont souvent utilisées par les outils de construction des analyseurs syntaxiques.
C’est le cas avec les analyseurs LR qui sont des analyseurs plus généraux que les analyseurs descendants.
VT est le vocabulaire terminal contenant l’ensemble des symboles atomiques individuels, pouvant être
composés en séquence pour former des phrases et souvent notés en lettres minuscules.
Selon la forme des règles de production, il existe quatre types de grammaires que nous présentons selon la
classification de Chomsky. Dans la suite nous considérons que si V est un vocabulaire alors V* est l’ensemble de
toutes les chaînes résultant de la concaténation des éléments de V.
Un symbole A VN est noté par <A> ou A (en utilise des lettres majuscules)
Un symbole a VT est noté sans les < > ou encore par "a" ou a (en utilise des lettres minuscules)
Exemple
<Inst> ::= id := <Exp> | if <Exp> then <Inst> | if <Exp> then <Inst> else <Inst>
Une partie qui peut apparaître de façon répétée est notée entre {} (apparaît 0 ou N fois)
Exemple
L’exemple présenté avec la notation BNF peut être noté comme suit en utilisant EBNF :
Bloc
begin end .
Inst
;
Inst
id := Exp
a- Dérivation
On appelle dérivation, l’application d’une ou plusieurs règles à partir d’une phrase (chaîne) de (VT VN)+.
On note une dérivation obtenue par l’application d’une seule règle de production et on *note une
dérivation obtenue par l’application de plusieurs règles de production.
Exemple
SA
A bB
Compilation, L3 Informatique , Dr Sari&Souici, Université d’Annaba, 2020/2021 28
Bc
La chaîne bc peut être dérivée par l’application de ces règles comme suit :
SA
bB
bc
On peut noter ces dérivations par une expression plus condensée S * bc
D’une manière générale, si G est une grammaire définie par (VT, VN, S, P) alors le langage généré peut être
* S x }.
noté par : L(G) = {x VT tel que
Dérivation gauche (la plus à gauche ou « left most ») : Elle consiste à remplacer toujours le symbole
non terminal le plus à gauche.
Dérivation droite (la plus à droite ou« right most ») : Elle consiste à remplacer toujours le symbole non
terminal le plus à droite.
Exemple
c S S
c a T b
S Mot analysé
c
Figure 3.2. Exemple d’arbre de dérivation
c- Arbre abstrait
On appelle arbre syntaxique abstrait ou arbre abstrait une forme condensée d’arbre syntaxique qui est
utilisée dans les compilateurs. L’arbre abstrait est obtenu par des transformations simples de l’arbre de
dérivation.
L’arbre syntaxique abstrait est plus compact que l’arbre syntaxique concret et contient des informations sur la
suite des actions effectuées par un programme. Dans un arbre abstrait, les opérateurs et les mots clés
apparaissent comme des nœuds intérieurs et les opérandes apparaissent comme des feuilles.
Exemple
E +
E + T a *
T T * F a a
a a
Arbre syntaxique concret
d- Grammaire ambiguë
On dit qu’une grammaire est ambiguë si elle produit plus d’un arbre syntaxique pour une chaîne donnée.
E E
E + E E * E
id E * E E + E id
id id id id
L’élimination de l’ambiguïté est une tâche importante qu’on exige généralement pour les langages de
programmation qui sont basés sur des grammaires non contextuelles. L’élimination de l’ambiguïté peut avoir
lieu de deux façons :
1. Elimination de l’ambiguïté par ajout de règles. Dans cette approche, la grammaire reste inchangée,
on ajoute, à la sortie de l’analyseur syntaxique, quelques règles dites de désambiguïté, qui éliminent les arbres
syntaxiques non désirables et conservent un arbre unique pour chaque chaîne. On peut, par exemple, éliminer
l’ambiguïté en affectant des priorités aux opérateurs arithmétiques. Dans les expressions arithmétiques, la
priorité est affectée aux parenthèses, puis au signe mois unaire, puis à la multiplication/division et enfin à
l’addition/soustraction.
2. Réécriture de la grammaire. Il s’agit de changer la grammaire en incorporant des règles de priorité et
d’associativité. Autrement dit, on ajoute de nouvelles règles et de nouveaux symboles non terminaux. La
grammaire ambiguë donnée précédemment peut être désambiguïsée comme suit :
EE+T|E–T|T
TT*F|T/F|F
F -F | L
L (E) | id
La chaîne id + id * id a maintenant une seule séquence de dérivations (gauches) :
E E + T T + T F + T L + T id+ T id + T*F
id + F*F id + L*F id + id*F id + id*L id + id*id
et l’unique arbre syntaxique donné par la figure 3.4.
E + T
T T F
*
F F L
L L id
id id
Exercice 2
1) Soit la grammaire des expressions arithmétiques G = ({+, -, *, /, a, b, c, (, ) }, {E}, E, P) où P est défini
par :
E E + E | E – E | E * E | E / E | (E) | -E | a | b | c
Donner l’arbre de dérivation de la chaîne b + a * b - c ? Que peut-on en déduire ?
2) Soit la grammaire G’ = = ({+, -, *, /, a, b, (, ) }, {E, T, F, L}, E, P) où P est défini par :
EE+T|E–T|T
TT*F|T/F|F
F -F | L
L (E) | a | b | c
a) Donner l’arbre de dérivation de la chaîne b + a * b - c. Que peut-on en déduire ?
b) Donner l’arbre abstrait correspondant à la chaîne b + a * b - c.
c) Donner les arbres syntaxiques concret et abstrait de la chaîne b - a + c.
Exercice 3
1) Soit la grammaire des expressions booléennes G = ({ou , et, non, vrai, faux, (, ) }, {A}, A, P) où P est défini
par :
A A ou A | A et A | non A | (A) | vrai | faux
Donner l’arbre de dérivation de la chaîne non faux ou vrai et vrai ? Que peut-on en déduire ?
2) Proposer une grammaire G’ pour éliminer le problème posé par la grammaire G.
3) Donner l’arbre de dérivation de la chaîne non faux ou vrai et vrai en utilisant la grammaire G’.
Exercice 4
Soit la grammaire G d’un langage proche de Pascal, exprimée sous forme EBNF de la manière suivante,
exprimer la grammaire G sous forme de diagrammes syntaxiques.
<Programme> ::= Program ident ; <Bloc>.
<Bloc> ::= [ Const <SuitConst> ; ] [ Var <SuitVar> ; ] { Procedure ident ; <Bloc> ;}
Begin <Inst> {; <Inst> } End
<SuitConst> ::= <DecConst> { , < DecConst>}
<DecConst> ::= ident = nbEnt
<SuitVar> ::= ident {, ident}
<Inst> ::= ident := <Exp> | If <Cond> Then <Inst> [Else <Inst>]
| Repeat <Inst> Until <Cond> | While <Cond> Do <Inst> | Begin <Inst> {; <Inst> } End
<Cond> ::= <Exp> ( = | <> | < | > | <= | >= ) <Exp>
<Exp> ::= <Terme> { (+ | -) <Terme>}
<Terme> ::= <Facteur> { (* | /) <Facteur>}
<Facteur> ::= ident | nbEnt | ( <Exp> )
L’analyse descendante part de l’axiome et tente de créer une chaîne identique à la chaîne d’entrée par
dérivations successives. Ainsi, l’opération de base est le remplacement d’un symbole non terminal par une de
ses parties droites. Quand l’analyseur doit remplacer un non terminal ayant plus d’une partie droite, il doit
pouvoir effectuer le bon choix. Dans ce chapitre, une méthode descendante d’analyse syntaxique va être
exposée, il s’agit de la méthode d’analyse prédictive non récursive.
Table
d’analyse M
Tampon d’Entrée
Programme
a + b $ d’analyse Flot de Sortie
prédictive
X
Y
Pile a
Z
+
$
b
Figure 4.1. Modèle d’analyseur prédictif non récursif
Le tampon d’entrée contient la chaîne à analyser suivie du symbole $ qui est un marqueur de fin de
chaîne.
La pile contient une suite de symboles grammaticaux, avec $ marquant le fond de la pile. Initialement,
l’axiome S se trouve au dessus de $.
La table d’analyse M qui est une matrice où un élément M[X,a] correspond à un non terminal X, qu’on
doit dériver, et à un terminal a (ou $), qu’on est en train de lire. Si on est arrivé à une étape où on doit
dériver le non terminal X et qu’on lit le caractère a dans la chaîne d’entrée, on doit utiliser la règle de
production qui se trouve dans M[X, a].
Le programme d’analyse prédictive contrôle l’analyse syntaxique en considérant le symbole en sommet
de pile X et le symbole d’entrée courant a. Ainsi, l’action de l’analyseur est déterminée par X et a, il y a
plusieurs cas possibles (voir la section 4.4).
Premier () = {x VT /
* x, , (VTUVN)*} sachant que : Si
* alors Premier ()
Exemple 1
Soient les règles de productions suivantes pour une grammaire donnée :
S Ba
B cP | bP | P|
P dS
Pour calculer Prem(X), il faut appliquer les trois règles suivantes jusqu’à ce qu’aucun terminal (ni ) ne puisse
être ajouté à Prem(X).
Détermination de Prem(X)
Si X VT alors Prem(X)={X}
Exemple 2
Soient les règles de productions suivantes pour une grammaire donnée :
S ABCe
A aA |
B bB | cB |
C de | da | dA
Prem(C) = {d}
Prem(B) = {b, c, }
Prem(A) = {a, }
Prem(S) = {a} U {b, c} U {d} = {a, b, c, d}
Exemple 1
Soient les règles de productions suivantes pour une grammaire donnée :
S aAB | aAd
B bB| c
On se propose de déterminer les suivants du symbole A. On constate, d’après la première règle de production,
que A peut être suivi par B ou d. D’après la deuxième règle, B peut commencer par b ou c. On peut donc
déduire que : Suiv(A)= {b, c, d}
Pour calculer les suivants pour tous les non terminaux, il faut appliquer les trois règles suivantes jusqu’à ce
qu’aucun terminal ne puisse être ajouté aux ensembles Suiv.
Pour chaque production A B, le contenu de Prem() sauf est ajouté à Suiv(B)
S’il existe une production A B ou une production A B telle que Prem() contient ( )
*
Alors les éléments de Suiv(A) sont ajoutés à Suiv(B)
Exemple 2
Soient les règles de productions suivantes pour une grammaire donnée :
S aSb | cd | SAe
A aAdB |
B bb
Suiv(S) = {$, b, a, e}
Suiv(A) = {e, d}
Suiv(B) = {e, d}
Un problème se pose quand ou *, dans ce cas, il faudra développer A en si le symbole d’entrée
courant est dans Suiv(A) ou si le $ a été atteint et que $ Suiv(A).
1er cas : Si X=a=$ Alors l’analyseur s’arrête : analyse réussie, chaîne acceptée
4.5.1 Exemple 1
Soient les règles de productions suivantes pour une grammaire donnée dont l’axiome est E :
E TE’
E’ +TE’ |
T FT’
T’ *FT’ |
F (E) | id
On se propose de construire la table d’analyse prédictive puis d’analyser les chaînes id+id*id et id++id en
utilisant la méthode d’analyse prédictive non récursive.
Pour cela, on commence d’abord par la détermination les premiers et les suivants pour chaque symbole non
terminal de la grammaire afin de faciliter la construction de la table d’analyse. Par la suite, le programme
d’analyse va être appliqué pour analyser chacune des chaînes demandées.
Prem Suiv
E (, id $, )
E’ +, $, )
T (, id +, $, )
T’ *, +, $, )
F (, id *, +, $, )
T E’
F T’ + T E’
id ε F T’ ε
id * F T’
id ε
Figure 4.2. Arbre syntaxique obtenu par l’analyseur prédictif pour la chaîne id+id*id
4.5.2 Exemple 2
Soient les règles de productions suivantes pour une grammaire donnée dont l’axiome est S, on veut construire
la table d’analyse prédictive pour cette grammaire:
S aAb
A cd | c
Prem Suiv
S a $
A c b
Définition : On appelle grammaire LL(1) une grammaire pour laquelle la table d’analyse prédictive ne contient
aucune case définie de façon multiple, chaque case de la table contient au plus une règle de production.
L : Left to right scanning (on parcourt ou on analyse la chaîne en entrée de la gauche vers la droite)
L : Leftmost derivation (on utilise les dérivations gauches)
1 : on utilise un seul symbole d’entrée de prévision à chaque étape nécessitant la prise d’une décision d’action
d’analyse.
C1 : Pour aucun terminal a, et ne se dérivent toutes les deux en des chaînes commençant par a.
Autrement dit : Prem() Prem() = .
C3 : Si (* ), ne se dérive pas en une chaîne commençant par un terminal de Suiv(A).
Autrement dit : Si (* ) alors Prem() Suiv(A) = .
Exemple 1
On se propose de montrer si la grammaire G1 ayant les règles de productions suivantes est LL(1) (c’est la
grammaire de l’exemple 2, section 4.5.2).
S aAb
A cd | c
Pour la production A cd | c, on constate que Prem(cd) Prem(c) = {c} {c} = {c} , ceci signifie que la
condition C1 n’est pas vérifiée donc la grammaire G1 n’est pas LL(1).
Exemple 2
Soient les règles de productions suivantes pour une grammaire G2 dont l’axiome est E (c’est la grammaire de
l’exemple 1, section 4.5.1):
E TE’
E’ +TE’ |
T FT’
T’ *FT’ |
F (E) | id
Conclusion : Les trois conditions (C1, C2 et C3) sont vérifiées pour toutes les productions de la forme A|,
donc la grammaire G2 est LL(1).
Ainsi, si la table d’analyse prédictive d’une grammaire comporte des cases définies de façon multiple, cette
grammaire n’est pas LL(1) mais on peut essayer de la transformer en une grammaire LL(1) en effectuant les
transformations suivantes :
1. La rendre non ambiguë. Pour cela, il n’existe malheureusement pas de méthode : une
grammaire ambiguë est une grammaire mal conçue. Il faut tenter de refaire la conception de
la grammaire (tenir compte de la priorité des opérateurs, associer le premier else au dernier
if…).
2. Eliminer la récursivité à gauche si cela est nécessaire (voir section 4.6.2.1).
3. Factoriser les règles de production à gauche si cela est nécessaire (voir section 4.6.2.2).
Par la suite, on peut construire la table d’analyse prédictive de la grammaire obtenue après transformations. Si
aucune case n’est définie de manière multiple, alors la grammaire obtenue est LL(1) et la méthode d’analyse
prédictive peut être appliquée.
Malheureusement, il existe des grammaires pour lesquelles il n’y a pas de transformation les rendant LL(1),
dans ce cas, la méthode d’analyse prédictive ne peut être appliquée et il faudra utiliser une autre méthode
d’analyse.
Dans le cas le plus simple, il s’agit d’une récursivité à gauche immédiate avec la présence d’un non
terminal A et une production de la forme A A. Pour éliminer la récursivité à gauche immédiate, il
faut remplacer toute production de la forme :
D’une manière plus générale, pour éliminer la récursivité à gauche immédiate, il faut procéder
comme suit :
A’ 1A’ |2A’ | …| M A’ |
Exemple 1
Après l’élimination de la récursivité à gauche, on obtient une grammaire G’ 1 équivalente à G1, dont les règles de
production sont :
E TE’
E’ +TE’ |
T FT’
T’ *FT’ |
F (E) | id
Exemple 2
Après l’élimination de la récursivité à gauche, on obtient une grammaire G’ 2 équivalente à G2, dont les règles de
production sont :
S BS’
S’ cAS’ |
A A’
A’ aA’ |
B dB’ | eB’
B’ bB’ |
Exemple 3
Soit la grammaire G3 ayant les règles de production suivantes :
Z Aa | z
A Ac | Zd
On obtient ainsi une grammaire G’3 équivalente à G3, non récursive à gauche et dont les règles de production
sont :
Z Aa | z
A zdA’
A’ cA’ | adA’ |
Exemple 4
Soit la grammaire G4 ayant les règles de production suivantes :
S Aa | b
A Ac | Sd | BA | c
B SSc | a
On obtient ainsi une grammaire G’4 équivalente à G4, non récursive à gauche et dont les règles de production
sont :
S Aa | b
A bdA’ | BAA’ | cA’
A’ cA’ | adA’ |
B bdA’aScB’ | cA’aScB’ | bScB’ | aB’
B’ AA’aScB’ |
Une grammaire est dite propre si elle ne contient aucune production A . Pour rendre une grammaire
propre, il faut rajouter une production dans laquelle le A est remplacé par , ceci pour chaque A apparaissant
en partie droite d’une production, et pour chaque A d’une production A .
A’ 1 | 2 |…| N
où (VT VN)+ et représente toutes les alternatives qui ne commencent pas par .
Remarque :
Pour chaque non terminal A, il faut essayer de trouver le plus long préfixe commun à deux ou plusieurs des
parties droites des A-productions. Il faut refaire l’opération de factorisation jusqu’à ne plus trouver de préfixes
communs.
Exemple 2
Soit la grammaire G2 ayant les règles de production suivantes :
S aEbS | aEbSeB | a
E bcB | bca
B ba
Finalement, la grammaire G’2 factorisée à gauche et équivalente à G2, aura pour règles de production :
S aS’’
S’’ EbSS’ |
S’ | eB
E bcE’
E’ B | a
B ba
Exercice 1
Déterminez, en donnant des explications, les ensembles de Premiers et de Suivants de chacun des non
terminaux des grammaires suivantes, S étant toujours l’axiome :
SaABb SaxBAC SA S AB
AAef ABSy A Ad Ae aB aC A CD CB b
BCD BASCAAz B bBC f B aE aB
Cg CSSt C g Cb
Dh D De f
Ea
Exercice 2
Soit la grammaire G1 = ({a, (, -, )},{A, B, C, D}, A, P)
P: A A (A) CB 1. Déterminer les premiers et les suivants de chaque non terminal.
B A 2. Etablir la table d'analyse prédictive de G1.
CaD 3. Analyser, en utilisant la méthode d’analyse prédictive, la chaîne a(a)a
D (A) et déduire son arbre de dérivation. Analyser aussi la chaîne –a.
Exercice 3
Montrer si les grammaires G1, G2, G3 , G4 et G5 ayant pour axiomes respectifs S1, S2, S3, S4 et S5 sont des
grammaires LL(1). Pour celles qui ne le sont pas, effectuer les transformations nécessaires pour qu'elles le
deviennent et montrez si les nouvelles grammaires obtenues sont LL(1) ou pas.
G1: S1 aAc G2: S2 BcBd CdCc G3: S3 De hDf Ef hEe G4: S4 FG G5: S5 aTb
A Abb b B D g F i T c S5 a
C E g G j
Exercice 4
Soit la grammaire G2 = ({a, b, (, ) , ,}, {S, T}, S, P), ayant les règles de production suivantes :
S a | b | (T)
T T,S | S
1. Montrer si la grammaire G2 est une grammaire LL(1). Si elle ne l’est pas, effectuer les transformations
nécessaires pour qu'elle le devienne. Soit G’2 la grammaire obtenue après ces éventuelles transformations.
2. Déterminer les premiers et les suivants de chaque non terminal de G’2.
3. Etablir la table d'analyse prédictive de G’ 2.
4. Analyser les chaînes (a,b) puis (a,(b) et (a,(a,b)) en utilisant l’analyse prédictive et déduire leurs arbres
syntaxiques.
Exercice 5
Soit la grammaire suivante (des expressions booléennes) dont l’axiome est A:
A A ou B | B
B B et C | C
C non C | D
D (A) | vrai | faux
1. Eliminer la récursivité à gauche et factoriser si cela est nécessaire.
2. Donner la table d’analyse de la nouvelle grammaire. Est-elle LL(1) ?
3. Expliciter le comportement de l’analyseur sur la chaîne non (vrai ou faux) et vrai.
Exercice 6
Soit la grammaire suivante dont l’axiome est S:
S Ab a AA
A Sa Ac B
B Sd
1. Eliminer la récursivité à gauche de cette grammaire.
2. Factoriser les règles obtenues si cela est nécessaire.
3. La grammaire obtenue est-elle une grammaire LL(1) ?
Le modèle général utilisé en analyse ascendante est le modèle par décalage-réduction (shift-reduce) qui
autorise deux opérations :
réduire (reduce) : réduire une chaîne par un non terminal en utilisant une des règles de production,
sachant que la chaîne réduite est une suite de terminaux et non terminaux à gauche du pointeur sur
l’entrée et finissant sur ce pointeur.
Exemple
Soit la grammaire G ayant les règles de production suivantes :
S aABe
A Abc| b
B d
On se propose d’analyser la chaîne abbcde de manière ascendante.
S
abbcde décaler
abbcde réduire
A
aAbcde décaler
aAbcde décaler B
A
aAbcde réduire
a A de décaler
a A de réduire
a A Be décaler a b b c d e
a A Be réduire
S Analyse réussie
Figure 5.1. Arbre syntaxique obtenu de manière
ascendante pour la chaîne abbcde
En résumé, on a donc les réductions suivantes, à partir desquelles on peut déduire, de manière ascendante,
l’arbre syntaxique correspondant à la chaîne abbcde (voir figure 5.1) :
abbcde
aAbcde
a A de
a A Be
S
On constate que la séquence de réductions précédentes correspond, en sens inverse, aux dérivations droites
suivantes : S aABe aAde aAbcde abbcde
Une table d’analyse dont le contenu diffère selon le type d’analyseur LR.
On distingue trois techniques de construction de tables d’analyse LR pour une grammaire donnée :
Simple LR (SLR) : qui est la plus simple à implémenter, mais la moins puissante des trois.
LookAhead LR (LALR) : qui a une puissance et un coût intermédiaires entre les deux autres et peut
être appliquée à la majorité des grammaires de langages de programmation.
Action Successeur
Xm
Pile …
S0
b
Figure 5.2. Modèle d’un analyseur LR
Le programme d’analyse range dans la pile des chaînes de la forme S 0X1S1X2S2…XmSm. Chaque Xi est un
symbole de la grammaire (Xi (VTUVN)) et chaque Si est un état (state) de l’analyseur qui résume l’information
contenue dans la pile au dessous de lui, la combinaison du numéro de l’état en sommet de pile et du symbole
d’entrée courant est utilisé pour indicer les tables et déterminer l’action à effectuer (décaler ou réduire).
Méthode : on commence avec S0 dans la pile, w$ dans le tampon d’entrée, le pointeur source ps est initialisé
au début de w. L’analyseur exécute la boucle Répéter jusqu’à ce qu’il rencontre une action Accepter ou Erreur.
Exemples
Remarque
Intuitivement, un item indique la «quantité» de partie droite qui a été reconnue à un instant donné de l’analyse.
Par exemple, l’item AX.YZ indique qu’on vient de voir en entrée une chaîne dérivée de X et qu’on espère
maintenant voir une chaîne dérivée de YZ.
Règle 2 : si A.B Ferm(I) et B est une production de G, alors ajouter B. à Ferm(I), s’il n’y existe
pas déjà. La règle 2 doit être appliquée jusqu’à ne plus pouvoir ajouter de nouveaux items à Ferm(I).
Fonction Ferm(I);
Début
J :=I ;
Répéter
Pour chaque item A.B J et chaque production B telle que B. J
Faire Ajouter B . à J
Jusqu’à ce qu’aucun item ne puisse être ajouté à J ;
Ferm(I) :=J;
Fin ;
Exemple
Soit la grammaire G, ayant les règles de production suivantes :
E E+T | T
T T*F | F
F (E) | id
Soit l’ensemble d’items I= {[E’.E]}. On se propose de calculer Ferm(I). En appliquant les deux règles
précédentes, on trouve : Ferm(I) = { [E’.E], [E.E+T], [E.T], [T.T*F], [T.F], [F.(E)], [F.id] }
Compilation, L3 Informatique , Dr Sari&Souici, Université d’Annaba, 2020/2021 50
5.3.1.4 Fonction Transition
Soit I un ensemble d’items et X un symbole de la grammaire (XVTUVN). Transition (I, X) est définie comme
étant la fermeture de tous les items [AX.] tels que [A.X] I.
Exemple
Procédure CollectionEnsembleItems;
Début
C := {Ferm({[S’.S]})} ;
Répéter
Pour chaque ensemble d’items I de C et pour chaque symbole
de la grammaire X tel que Trans(I, X) soit non vide et non encore dans C
Faire ajouter Trans(I, X) à C
Jusqu’à ce qu’aucun ensemble d’items ne puisse être ajouté à C;
Fin ;
Exemple
On se propose de déterminer la collection canonique d’ensembles d’items de la grammaire augmentée
suivante :
E’E
E E+T | T
T T*F | F
F (E) | id
I0 = Ferm {[E’.E]}
= E’.E
E.E+T
E.T
T.T*F
T.F
F.(E)
F.id
F
vers I3
(
vers I4
id
vers I5
T * F
I2 I7 I10
(
vers I4
id
vers I5
F
I3
(
( E )
I4 I8 I11
+
id vers I6
T
vers I2
F
vers I3
id
I5
Construire C la collection canonique d’ensembles d’items LR(0) pour G’, soit C={I0, I1, I2, …….IN}
Algorithme ConstructionTableSLR;
Début
1/ L’état i est construit à partir de Ii. La partie ACTION pour l’état i est déterminée comme suit:
a. Si [A.a] Ii et Trans (Ii, a)= Ij (avec a VT) Alors ACTION [i, a] dj (décaler j)
b. Si [A.] Ii (avec AS’) Alors ACTION [i, a] rnum pour tout a Suiv(A)
(réduire par la régle A dont le numéro est num )
c. Si [S’S.] Ii Alors ACTION [i, $] accepter
2/ La partie SUCCESSEUR pour l’état i est déterminée comme suit :
Si Trans (Ii, A)= Ij Alors SUCCESSEUR [i, A] j
3/ Toutes les entrée restantes dans la table sont mises à ERREUR
4/ L’état initial de l’analyseur est construit à partir de l’ensemble d’items contenant [S’.S]
Fin ;
Exemple
S’S
S G=D
SD
G *D
G id
D G
Détermination de la collection canonique d’ensembles d’items :
Dans notre cas, la grammaire n’est pas ambiguë mais elle n’est pas SLR(1). D’ailleurs, beaucoup de grammaires
non ambiguës ne sont pas SLR(1). Le conflit shift/réduce (décaler/réduire) dans l’exemple précédent provient
du manque de puissance de la méthode SLR qui ne peut pas « se rappeler » assez de contexte gauche pour
décider de l’action de l’analyseur sur l’entrée « = » après avoir « vu » une chaîne pouvant être réduite vers G.
Les méthodes LR canonique et LALR réussissent pour un nombre plus important de grammaires
que la méthode SLR.
Fonction Ferm(I);
Début
Répéter
Pour chaque item [A.B, a] I, chaque production B G’
et chaque terminal bPrem(a) tel que [B., b] I
Faire Ajouter [B ., b] à I
Jusqu’à ce qu’aucun item ne puisse être ajouté à I;
Ferm(I) := I;
Fin ;
Procédure CollectionEnsembleItems;
Début
C := {Ferm({[S’.S, $]})} ;
Répéter
Pour chaque ensemble d’items I de C et pour chaque symbole
de la grammaire X tel que Trans(I, X) soit non vide et non encore dans C
Faire ajouter Trans(I, X) à C
Jusqu’à ce qu’aucun ensemble d’items ne puisse être ajouté à C;
Fin ;
Algorithme ConstructionTableLR;
Début
1/ L’état i est construit à partir de Ii. La partie ACTION pour l’état i est déterminée comme suit:
a. Si [A.a, b] Ii et Trans (Ii, a)= Ij (avec a VT) Alors ACTION [i, a] dj (décaler j)
b. Si [A., a] Ii (avec AS’) Alors ACTION [i, a] rnum
(réduire par la régle A dont le numéro est num )
c. Si [S’S., $] Ii Alors ACTION [i, $] accepter
2/ La partie SUCCESSEUR pour l’état i est déterminée comme suit :
Si Trans (Ii, A)= Ij Alors SUCCESSEUR [i, A] j
3/ Toutes les entrée restantes dans la table sont mises à ERREUR
4/ L’état initial de l’analyseur est construit à partir de l’ensemble d’items contenant [S’.S, $]
Fin ;
Toute grammaire SLR(1) est une grammaire LR(1), mais, pour une grammaire SLR(1), l’analyseur
LR canonique peut avoir un nombre d’états supérieur à l’analyseur SLR pour la même grammaire.
L’idée générale de cet algorithme est de construire les ensembles d’items LR(1) et de fusionner les ensembles
ayant un cœur commun. La table d’analyse LALR(1) est construite à partir de la collection des ensembles
d’items fusionnés. Signalons que cet algorithme est le plus simple, mais il existe d’autres algorithmes plus
efficaces pour la construction de tables LALR(1), qui ne se basent pas sur la méthode LR(1).
Algorithme ConstructionTableLALR;
Début
1/ Construire la collection d’ensembles d’items LR(1) pour la grammaire augmentée G’.
2/ Pour chaque cœur présent parmi les ensembles d’items LR(1), trouver tous les états ayant ce même
cœur et remplacer ces états par leur union.
3/ La table LALR(1) est obtenue en condensant la table LR(1) par superposition des lignes correspondant
aux états regroupés.
Fin ;
Pendant la superposition des lignes, il y a un risque de conflit :
Conclusion: Pendant la superposition des lignes, le seul cas de conflit qui peut se produire est réduire/réduire.
Remarque
S’il n’y a pas de conflit dans la table LALR(1), la grammaire sera considérée LALR(1) ou LALR.
Pour la détermination de la collection d’ensembles d’items LALR(1), on recherche les ensembles d’items LR(1)
pouvant être fusionnés, on en trouve trois paires : I3 avec I6, I4 avec I7 et I8 avec I9 qui seront remplacés par
leurs unions respectives :
La construction de la table d’analyse LALR est obtenue par superposition des lignes correspondant aux états
fusionnés:
Action Successeur
Etat
c d $ S C
0 d36 d47 1 2
1 acc
2 d36 d47 5
36 d36 d47 89
47 r3 r3 r3
5 r1
89 r2 r2 r2
Si la chaîne analysée est erronée syntaxiquement (n’appartient pas au langage), l’erreur sera détectée plus
rapidement par l’analyseur LR(1) alors que l’analyseur LALR(1) peut effectuer plusieurs réductions avant de
signaler l’erreur.
Ces deux cas sont illustrés à travers la réponse aux questions 4 et 5 de l’exercice 6.
Exercice 3
Soit la grammaire ayant les règles de production suivantes :
S if a then S | if a then S else S | i
1. Construire l’arbre de dérivation de la chaîne if a then if a then i else i. Que peut-on conclure ?
2. Cette grammaire peut-elle être une grammaire SLR ? Justifier votre réponse.
3. Construire la table d’analyse SLR pour cette grammaire.
4. Utiliser cette table pour analyser la chaîne if a then if a then i else i en résolvant les conflits éventuels.
Exercice 4 (à faire à la maison, pas de correction en TD, quelques indications de réponses dans l’énoncé de
l’exercice)
Soit la grammaire G = ({debut, fin, ., ;, si, alors, sinon, a, :=, et }, {A, L, I, E}, A, P) où P est défini par les règles de
production suivantes :
A debut L fin .
L L;I|I
I si E alors I | si E alors I sinon I | a := E
E E et E | a
1. Construire la collection d’ensembles d’items LR(0) pour cette grammaire.
(Réponse partielle : on trouve 21 ensembles d’items de I0 jusqu’à I20)
2. Dresser la table d'analyse syntaxique SLR. (Réponse partielle : on trouve 2 conflits d/r)
3. Utiliser cette table pour analyser la chaîne : debut si a et a alors a := a sinon a := a fin. puis déduire son arbre
de dérivation. (Réponse partielle : la chaîne est acceptée après 26 étapes d’analyse)
6.1 Introduction
La grammaire hors contexte d’un langage de programmation ne peut pas décrire certaines propriétés
fondamentales qui dépendent du contexte telles que :
L’impossibilité d’utiliser dans ses instructions une variable non déclarée préalablement
La nécessité de correspondance (en nombre et en type) entre les paramètres formels et les
paramètres effectifs des procédures et des fonctions
L’analyse sémantique (contextuelle) a pour rôle de vérifier ce genre de contraintes. Elle se fait, en général, en
associant des règles sémantiques aux productions de la grammaire. Deux notations peuvent être utilisées pour
effectuer cette association :
Les règles sémantiques calculent la valeur des attributs attachés au symbole de la grammaire.
Conceptuellement la chaîne en entrée est analysée syntaxiquement pour construire un arbre qui est parcouru
autant de fois que nécessaire pour évaluer les règles sémantiques à ses nœuds. L’évaluation des règles
sémantiques peut produire du code, sauvegarder de l’information dans la table des symboles, etc., pour obtenir
la traduction de la chaîne en entrée.
6.2.1 Introduction
Une DDS est un formalisme permettant d’associer des actions à une règle de production de la grammaire. On
appelle ainsi DDS la donnée d’une grammaire et des règles sémantiques. On parle de grammaire attribuée.
Une règle sémantique est une suite d’instructions algorithmiques (elle peut contenir des
affectations, des instructions d’affichage, des instruction de contrôle, etc.)
Les règles sémantiques impliquent des dépendances entre les attributs. Ces dernières sont représentées par un
graphe de dépendance (GD) à partir duquel en déduit un ordre d’évaluation des règles sémantiques.
Compilation, L3 Informatique , Dr Sari&Souici, Université d’Annaba, 2020/2021 62
Remarque
Un arbre syntaxique muni des valeurs d’attributs à chaque nœud est appelé arbre décoré (ou annoté). Le
processus de calcul des valeurs des attributs aux nœuds est appelé décoration (ou annotation) de l’arbre.
f : est une fonction, souvent écrite sous forme d’expressions et parfois sous forme d’un appel de procédure.
b : est soit un attribut synthétisé de A, soit un attribut hérité d’un symbole en partie droite de la règle.
C1, C2, …, Ck : sont des attributs de symboles quelconques dans la règle de production.
Les attributs synthétisés sont très utilisés en pratique. Une DDS qui utilise uniquement les attributs synthétisés
est appelée Définition S-attribuée.
Un arbre syntaxique pour une définition S-attribuée est toujours décoré en évaluant les règles sémantiques
pour les attributs de chaque nœud du bas vers le haut (peut être adapté à une analyse ascendante : grammaire
LR, par exemple).
Exemple
Soit la grammaire d’un programme de calculatrice de bureau :
L En
EE+T|T
TT*F|F
F (E) | Chiffre
Productions Règles sémantiques
L En Imprimer (E.val)
E E1 + T E.val := E1. val + T
ET E.val := T .val
T T1 * F T.val := T1.val * F.val
TF T.val := F.val
F (E) F.val :=E.val
F Chiffre F.val := Chiffre.valLex
Remarques
Cette DDS associe des attributs synthétisés « val » de type entier à chacun des non terminaux E, T et F.
Le terminal Chiffre a un attribut synthétisé valLex (valeur lexicale) dont la valeur est fournie par l’analyse
lexicale.
Exemple
Soit la chaîne 3*4+5n. On veut établir l’arbre syntaxique correspondant à cette chaîne, avant et après la
décoration, en utilisant la DDS précédente.
E n
E + T
T F
chiffre
T * F
F chiffre
chiffre
L.imprimer(17)
E.val=17 n
E.val=12 + T.val=5
T.val=12 F.val=5
F.val=3 chiffre.valLex=4
chiffre.valLex=3
Dans une DDS, les terminaux de la grammaire ne sont sensés avoir que des attributs synthétisés dont les
valeurs sont fournies, généralement, par l’analyseur lexical.
Exemple
La première règle sémantique L.Type h := T.Type, associée à la production D T L, donne à l’attribut hérité
L.Type h la valeur de type de la déclaration (entier ou réel). Les autres règles sémantiques font descendre ce
type le long de l’arbre par l’intermédiaire de l’attribut hérité L.Type h. La procédure ajouterType() est appelée
pour permettre d’ajouter le type de chaque identificateur au niveau de son entrée dans la table des symboles.
Exemple
T L
entier L , id
c
L , id
id b
T.type=entier L.typeh=entier
entier L.typeh=entier , id
c
L.typeh=entier , id
id b
Exemple 1
A chaque fois qu’on utilise la production E E1 + E2 dans un arbre syntaxique, on ajoute les deux arcs ci-
dessous (voir figure 6.5) au niveau du graphe de dépendance pour indiquer que l’évaluation de E.val doit être
effectuée après celle de E1.val et E2.val
E. val
X. x Y. y
X. h
Pour chaque règle sémantique b := f(c1, …, ck) associé à la production appliquée en n faire
La traduction dirigée par la syntaxe peut se baser sur les arbres abstraits, de la même façon que sur les arbres
syntaxiques concrets, en attachant des attributs aux nœuds. L’utilisation des arbres abstraits comme
représentation intermédiaire permet de dissocier la traduction de l’analyse syntaxique.
Exemple 1
Soit à représenter l’arbre syntaxique concret et l’arbre abstrait de la chaîne 3 * 4 + 5 en utilisant la grammaire :
EE+T|T
TT*F|F
F Chiffre
E + T
* 5
T F
chiffre 3 4
T * F
F chiffre
chiffre
Figure 6.7. Arbre syntaxique concret et arbre abstrait de la chaîne 3 * 4 + 5
Exemple 2
Si C Alors I1 Sinon I2
Si Alors Sinon
C I1 I2
Chaque nœud de l’arbre abstrait peut être implémenté sous la forme suivante :
Pour créer les nœuds des arbres abstraits correspondant à des expressions avec des opérateurs binaires, on
utilise les fonctions suivantes :
1- CréerNoeud (Op, gauche, droit) : Crée un nœud opérateur ayant une étiquette op avec deux champs
contenant des pointeurs vers gauche et droit.
Exemple
On veut déterminer la séquence d’appels pour créer l’arbre abstrait représentant l’expression a - 4 + c
P1 := CréerFeuille(id, a)
P2 := CréerFeuille(nb, 4)
P3 := CréerNoeud(‘-‘, P1, P2)
P4 := CréerFeuille(id , c)
P5 := CréerNoeud(‘+‘, P3, P4)
L’arbre abstrait correspondant est donné par la figure 6.9. L’arbre abstrait physique est illustré dans la figure
6.10.
- c
a 4
P5
‘+’
P3 P4
‘-’ id
P1 P2
Pointeur vers l’entrée de c
id nb 4 dans la table des symboles
En utilisant la DDS précédente, la création de l’arbre abstrait pour l’expression « a – 4 + c » sera effectuée (de
façon ascendante) de la manière suivante :
Association des règles sémantiques aux règles de production correspondantes dans DDS
La figure 6.11 donne la représentation physique de l’arbre abstrait pour l’expression en question.
E pnoeud
E pnoeud + T pnoeud
E - T pnoeud id
‘+’
T pnoeud nb
‘-’ id
id
La différence entre DAG et arbre abstrait est qu’un nœud du DAG représentant une sous-expression commune
a plus d’un père.
Exemple
+ +
* * * *
- - -
a a b c b c d a b c d
Les DDS peuvent être utilisés pour construire des DAG de la même manière que pour les arbres abstraits. Les
corps des fonctions créerNoeud et créerFeuille doivent être modifiés pour vérifier, avant la création, si le nœud
ou la feuille considérée n’a pas déjà été créée. Dans le cas de l’existence, ces fonctions retournent un pointeur
vers le nœud ou la feuille construite précédemment
On peut supposer que la pile est implémentée en utilisant deux tableaux « état » et « val ». Si état[i] contient
le symbole « X » alors val[i] contient la valeur de l’attribut attaché au nœud de l’arbre syntaxique
correspondant à ce « X » . A tout moment, le sommet de la pile est repéré par le pointeur sommet.
Exemple
Avant la réduction de XYZ en A , Val[sommet] contient Z.z, Val[sommet-1] contient Y.y, Val[sommet-2] contient
X.x
Etat Val
X X.x
Y Y.y
Sommet Z Z.z
Etat Val
Sommet A A.a
Début
Début
VisiterEnProfondeur(m) ;
Fin ;
Fin ;
Le code à trois adresses est une représentation intermédiaire bien adaptée à la complexité des problèmes des
expressions arithmétiques et aux structures de contrôle imbriquées.
Le code à trois adresses correspond à une représentation linéaire des arbres abstraits et des DAG dans laquelle
des noms explicites correspondent aux nœuds internes des graphes.
Un code à trois adresses est une séquence d’instructions de la forme x := y op z où x,y et z sont des noms de
constantes ou de variables temporaires produites par le compilateur. op étant un opérateur arithmétique ou
logique.
Exemple
Soit à trouver la représentation sous forme d’arbre abstrait et de code à trois adresses de l’instruction
d’affectation suivante : a := x + y * -z
:=
* t1 := -z
t2 := y * t1
t3 := x + t2
+ a := t3
a x y z
Une instruction à trois adresses est une forme abstraite de code intermédiaire qui peut être implémentée dans
un compilateur par des structures dont les champs contiennent l’opérateur, les opérandes et les résultats. Parmi
ces structures, on trouve les quadruplets, les triplets et les triplets indirects.
i * y t1 t2 t2 := y * t1
Remarque
Argument 1, Argument 2 et Résultat sont généralement des pointeurs sur les entrées (de la table des symboles)
qui correspondent aux nœuds représentés par ces champs.
Dans un schéma de traduction utilisant les quadruplets, on peut faire appel à une procédure permettant de
générer un quadruplet correspondant à Res := A1 op A2.
Début
TQ[quadSuiv].Opérateur := oper ;
TQ[quadSuiv].Argument1 := A1;
TQ[quadSuiv].Argument2 := A2;
TQ[quadSuiv].Résultat := Res ;
quadSuiv := quadSuiv + 1 ;
Fin ;
Remarque
Il existe aussi une fonction CréerTemp qui peut être utilisée dans les schémas de traduction, pour créer une
variable temporaire dont le nom doit être différent de ceux utilisés par le programmeur.
Début
CréerTemp := ‘$’+ConvEntChaîne(cpt) ;
cpt := cpt + 1 ;
Fin ;
cpt est un compteur de variables temporaires initialisé à zéro. ConvEntChaîne est une fonction qui convertit un
entier en une chaîne de caractères.
Ainsi, CréerTemp est une fonction qui permet de générer séquentiellement des noms de variables temporaires
$0 ; $1, $2, …
$0 := a*b
$1 := b-c
$2 := 3 * $1
$3 := $0 + $2
$4 := $3 + d
Pour chaque quadruplet, il faut créer une variable temporaire, donc, pour chaque règle de la
grammaire contenant un opérateur, l’action sémantique associée doit contenir une instruction de
création de variables temporaire et une instruction de génération de quadruplet.
Schéma de traduction
E E + E | E – E | E * E | E / E | (E) | -E | id | nb
Remarque
Pour pouvoir générer les quadruplets, il faut transmettre le nom de la variable résultat de l’expression E à l’unité
syntaxique qui contient E par l’intermédiaire de la traduction du nom associé à E.
Exemple
$0 := a*b
$1 := b-c
$2 := 3 * $1
$3 := $0 + $2
$4 := $3 + d
E $3
E $2
E $1
E $0 E $1
E a E b E 3 E b E c E d
a * b + 3 * ( b - c ) + d
$0 := a + b
$1 := $0 * c
a := $1
On remarque qu’il faut d’abord traduire l’expression (a + b) * c en code quadruplet puis générer le code
quadruplet qui affecte le résultat à a.
id a := E $1
E $0 * E c
( E $0 ) id c
E a + E b
id a id b
Figure 7.3. Arbre de dérivation de l’affectation a := (a + b) * c
avec l’évaluation de sa traduction en code quadruplet
On associe la variable temporaire $0 à l’expression a < b de telle sorte que si a est inférieur à b, $0 sera égale
à 1 et elle sera égale à 0 dans le cas contraire.
0 : if a < b goto 3
1 : $0 := 0
2 : goto 4
3 : $0 := 1
4 : c := $0
Le tableau suivant donne les schémas de traduction correspondant aux règles de production de la grammaire :
Exemple
$0 0 : if b < c goto 3
1 : $0 := 0
2 : goto 4
3 : $0 := 1
$1 4 : if d > f goto 7
5 : $1 := 0
6 : goto 8
7 : $1 := 1
$2 8 : if g = h goto 11
9 : $2 := 0
10 : goto 12
11 : $2 := 1
12 : $3 := $1 and $2
13 : $4 := $0 or $3
14 : a := $4
Exercice 1
Soit l’expression suivante :
a + a * (b – c) + (b – c) * d
Exercice 2
Soit l’affectation suivante :
i := i + 10
1. Représenter graphiquement l’arbre abstrait et le DAG correspondant à cette affectation
2. Donner la séquence d’appels permettant de construire le DAG.
3. Donner la structure physique du DAG en supposant que les nœuds sont implémentés en mémoire de
manière dynamique puis de manière statique.
Exercice 3
Donner le code quadruplet correspondant à l’affectation suivante :
x := (a+b*c) > (e*f/g) or not h and not (i ≤ j)