Poly Me03li 2
Poly Me03li 2
Poly Me03li 2
Introduction au parsing
Le non-déterminisme dans les analyses descendantes vient du fait que, étant donné un
non-terminal A à dériver, on a le choix entre toutes les parties droites des règles A → β. On
a vu que si β commence par un terminal, une décision sur la productivité de la dérivation
peut être prise immédiatement (en comparant ce terminal à la chaîne à reconnaître). Mais
si β commence par un non-terminal, il n’est pas facile de trouver comment prendre une
bonne décision.
L’idée des grammaires LL(k) est de construire, à partir de la grammaire initiale (sans la
modifier), une table de prédiction : étant donné le non-terminal à dériver, et le symbole
courant du mot à reconnaître, cette table donne les parties droites susceptibles de donner
finalement le symbole à reconnaître.
S’il n’y a qu’une dérivation possible dans chaque cas (au plus), la grammaire est dite
LL(1). Si on peut construire une table où il n’y a qu’une dérivation possible en regardant
2 caractères, la grammaire est dite LL(2).
Pourquoi ce nom LL(k) ? Parce que ces grammaires permettent directement de mettre en
œuvre des analyseurs construisant de manière déterministe de gauche à droite (left-to-right )
des dérivations gauches (left) avec un regard avant (look-ahead) de k symboles. (SLL(1)
désigne les grammaires simples de cette famille.)
Exemple LL(1) Comme premier exemple de table de prédiction (ou d’analyse prédic-
tive), considérons le cas de la grammaire suivante, qui est (presque) sous forme de Greibach
S → aSb ; S → cC ; C → dC ; C → c. La table représente, pour chaque non-terminal à
dériver, pour chaque lettre du mot à apparier, la (ou les) règle(s) de dérivation à appliquer.
a b c d
S aSb cC
C c dC
Exemple LL(1) (non Greibach) Cette grammaire n’est pas sous forme de Greibach,
ce qui ne l’empêche pas d’être LL(1) : S → aSb ; S → CC ; S → b ; C → dC ; C → c.
Langage engendré : an (d∗ cd∗ c|b)bn .
La table, qu’on peut facilement construire à la main en regardant la grammaire (même si
pour être sûr de ne pas faire d’erreur, il vaut mieux vérifier avec l’algorithme qu’on va voir
ensuite), montre bien que la grammaire est LL(1).
a b c d
S aSb b CC CC
C c dC
Dans ces deux exemples, il y a au plus une dérivation par case (les cases vides correspondent
à des cas d’erreur), il est donc possible de décider quelle dérivation appliquer en considérant
1 caractère de la chaîne à produire, ces grammaires sont LL(1).
Soit le mot aacddcbb, on peut vérifier facilement que l’analyse avec la première grammaire
sur la base de la table donne une décision unique à chaque étape : l’arbre d’exploration n’a
12
Université Paris Diderot – ME03LI – 15/16 Ch2. Introduction au parsing
Exemple LL(2) Avec la grammaire suivante, il y a une case qui contient deux règles.
S → aSb ; S → ab ; S → cC ; C → dC ; C → c.
a b c d
S aSb cC
ab
C c dC
On peut s’intéresser à la table de prédiction considérant 2 caractères en avant. Dans ce cas,
il faut aussi prendre en considération le fait que l’on peut se trouver dans une situation où
il ne reste plus qu’un seul caractère à produire : ces cas sont représentés par la notation $
qui correspond à la fin du mot.
aa ab ac cc cd c$ dc dd cb
S aSb ab aSb cC cC
C c dC dC c
Il y a au plus une dérivation par case : on dira que la grammaire est LL(2).
Dans les deux exemples précédents, la table était très facile à construire, grâce au fait que
les parties droites de règles commençaient (presque) toujours par un terminal. Cependant,
il est possible de construire la table de prédiction pour n’importe quelle grammaire (quitte
à ce qu’il y ait plusieurs règles dans certaines cases). Comment procéder ?
Intuitivement, il faut mettre en œuvre la récursivité de la grammaire : il y a bien des
règles qui produisent des terminaux à gauche de leur partie droite (la grammaire est non
13
Université Paris Diderot – ME03LI – 15/16 Ch2. Introduction au parsing
récursive gauche par hypothèse) — soit par exemple C → aα ; alors je peux considérer
comme pertinente pour produire un a les règles qui sont de la forme D → Cβ.
Cette observation nous met sur la voie d’une méthode récursive de construction de la table
LL, qui passe par la construction de deux ensembles associés à chaque symbole : premier()
et suivant() (first et follow ).
premier() est défini pour tout symbole de la grammaire, terminal ou non terminal, et par
extension, il peut être défini pour tout mot sur (X ∪ V )∗ , et donc en particulier pour toute
partie droite de règle.
∗
Pour α ∈ (X ∪ V )∗ , premier(α) = {a ∈ X / α → au}
Intuitivement, cette fonction associe à tout proto-mot son « coin gauche » : le premier
terminal du proto-mot dérivé. Si le proto-mot commence par un terminal, c’est trivial,
sinon, il faut regarder comment le non terminal finit par se réécrire (il peut y avoir plusieurs
étapes, mais si la grammaire est non récursive gauche, ça doit se terminer). Une fois
identifié, ce coin gauche ne peut pas changer (avec une grammaire algébrique : les terminaux
ne sont jamais effacés ou déplacés).
suivant() est défini seulement pour les non terminaux de la grammaire : il s’agit du
premier symbole qui peut suivre le mot produit par le non terminal.
∗
Pour A ∈ V , suivant(A) = {a ∈ X / S → αAaβ}
Par convention, on introduit la notation $ pour représenter le fait que la fin du mot peut
suivre un non terminal donné.
L’algorithme de construction d’une table LL(1) commence par le calcul de ces deux en-
sembles, qui en pratique doivent être calculés pour chaque non terminal (dans les autres
cas, premier() est trivial, et suivant() n’a pas besoin d’être calculé).
Le calcul des premier() se fait avec un algorithme de point fixe. Il faut réitérer la ma-
nœuvre suivante (donc pour chaque non terminal) jusqu’à ce qu’aucun changement ne soit
enregistré.
Pour chaque non terminal A :
Pour chaque règle A → α :
Si α = ε, ajouter {ε} à prem(A)
Sinon (alors α = A1 A2 ...Ak ) :
i=0
répeter :
i=i+1
ajouter prem(Ai ) \ {ε} à prem(A)
tant que i ≤ k et ε ∈ prem(Ai )
si i = k et ε ∈ prem(Ai ) :
ajouter {ε} à prem(A)
N.B. ce calcul nous donne la valeur de prem() pour n’importe quel A ∈ V , il peut
être facilement généralisé, avec les mêmes précautions concernant la présence d’ε dans
les prem(), et en supposant que si x ∈ X, prem(x) = {x}, au calcul de prem(β) pour
tout β ∈ (X ∪ V )∗ .
Le calcul de suiv() utilise le résultat de prem(). L’algorithme consiste aussi à réitérér le
calcul suivant (pour chaque règle) jusqu’au point fixe :
14
Université Paris Diderot – ME03LI – 15/16 Ch2. Introduction au parsing
Table LL(1) Une fois les deux ensembles (fonctions) prem() et suiv() construites, on
peut construire la table LL(1) :
Voir aussi la figure 2.13 pour une autre formulation des mêmes algorithmes.
2.2.1.3 Conclusion
Analyse LL(1) Il devient très facile, ainsi équipé d’une table de prédiction, de formuler
un algorithme de parsing descendant : cet algorithme est d’une complexité linéaire, puisque
chaque symbole terminal mène à une décision unique et non remise en cause.
L’implémentation d’un tel algorithme est laissée en exercice (il faut évidemment aussi
envisager une implémentation de la construction de la table...)
15
Université Paris Diderot – ME03LI – 15/16 Ch2. Introduction au parsing
16