Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% ont trouvé ce document utile (0 vote)
85 vues5 pages

Poly Me03li 2

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

Université Paris Diderot – ME03LI – 15/16 Ch2.

Introduction au parsing

2.2.1 Grammaires LL(k)

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

2.2.1.1 Tables de prédiction

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

qu’une branche, et l’analyse est linéaire : voir figure 2.12.

Figure 2.12 – Arbre d’exploration pour le mot aacddcbb et la grammaire S → aSb ;


S → cC ; C → dC ; C → c
(S , aacddcbb)
(aSb , aacddcbb)
(Sb , acddcbb)
(aSbb , acddcbb)
(Sbb , cddcbb)
(cCbb , cddcbb)
(Cbb , ddcbb)
(dCbb , ddcbb)
(Cbb , dcbb)
(dCbb , dcbb)
(Cbb , cbb)
(cbb , cbb)
(ε , ε)

Analogie avec l’automate : si on se souvient que la correspondance entre automates et


grammaires passe par la correspondance entre états et non terminaux, on voit que la table
construite ici ressemble beaucoup à la table de transition d’un automate... Ce n’est bien
sûr pas fortuit...

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

2.2.1.2 Construction d’une table LL(1)

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

Mettre $ dans suiv(S)


Pour chaque règle A → A1 A2 ...Ak :
Pour chaque Ai (i ∈ [1, k[) :
ajouter prem(Ai+1 ) \ {ε} à suiv(Ai )
Ajouter suiv(A) à suiv(Ak )
j=k
tant que ε ∈ prem(Aj ) (et j ≥ 0) :
ajouter suiv(A) à suiv(Aj−1 )
j = j −1

Table LL(1) Une fois les deux ensembles (fonctions) prem() et suiv() construites, on
peut construire la table LL(1) :

1. Pour la règle no i de la forme A → α :


(a) Pour tout a ∈ premier(α), ajouter i à la case (A, a).
(b) Si ε ∈ premier(α), ajouter i à la case (A, b) pour chaque b ∈ suivant(A).
Si ε ∈ premier(α) et $ ∈ suivant(A), ajouter i à la case (A, $).
2. Marquer erreur dans toutes les cases restées vides.

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

LL1-isation Les grammaires LL(1) sont particulièrement coopératives, mais malheu-


reusement on sait que toutes les grammaires algébriques ne sont pas équivalentes à une
grammaire LL(1). Il est cependant intéressant d’évoquer la ou les méthodes que l’on peut
utiliser pour se rapprocher d’une grammaire LL(1).
La première méthode consiste à supprimer les récursions gauches, ce qui est fait implicite-
ment dans le processus de mise sous forme normale de Greibach (on a vu l’algo indépen-
damment) ; une seconde transformation utile consiste à faire une factorisation gauche de
la grammaire.

Grammaires LL(k) Mais si ça facilite la construction de la table et peut dans certains


cas augmenter le déterminisme, il reste souvent des cases contenant plusieurs règles, et
donc des facteurs d’indétermination. Dans ce cas, il reste la possibilité de construire des
tables de prédiction en augmentant le regard en avant (cf exemple du début de la section).
Il faut noter que cette “généralisation”, d’une part augmente la complexité du pré-trai-
tement de la grammaire, et d’autre part (surtout), ne permet pas de traiter toutes les
grammaires. C’est évident pour les grammaires ambigües, mais c’est aussi le cas pour

15
Université Paris Diderot – ME03LI – 15/16 Ch2. Introduction au parsing

Figure 2.13 – Autres versions des algos prem/suiv/ll


Algorithme de construction de la table LL

Pour α ∈ (X ∪ V )∗ , premier(α) = {a ∈ X / α → au}

Pour A ∈ V , suivant(A) = {a ∈ X / S → αAaβ}
Calcul de premier(A) Réitérer jusqu’au point fixe :
1. Si A ∈ X, premier(A) = {A}.
2. Si A → ε ∈ P , ajouter ε à premier(A).
3. Pour les règles A → Y1 . . . Yk :
(a) Ajouter les symboles de premier(Y1 ) dans premier(A) ;
(b) S’il existe un intervalle [1..l] tel que ∀i ∈ [1..l], ε ∈ premier(Yi ), ajouter à
premier(A) tous les symboles des premier(Yi ) pour i ∈ [1..l + 1].
(c) Si k = l (ie ε appartient à tous les premier(Yi )), alors ajouter ε à pre-
mier(A).
Par extention (avec les mêmes précautions concernant ε), on peut calculer premier(β)
pour tout β ∈ (X ∪ V )∗ .

Calcul de suivant() Réitérer jusqu’au point fixe :


1. Mettre $ dans suivant(S).
2. Si A → αBβ, le contenu de premier(β) (sauf ε) est ajouté à suivant(B).
3. S’il existe une règle A → αB (ou une règle A → αBβ, avec ε ∈ premier(β)), les
éléments de suivant(A) sont ajoutés à suivant(B).

Construction de la table LL(1)


1. Pour la règle no i de la forme A → α :
(a) Pour tout a ∈ premier(α), ajouter i à la case (A, a).
(b) Si ε ∈ premier(α), ajouter i à la case (A, b) pour chaque b ∈ suivant(A).
Si ε ∈ premier(α) et $ ∈ suivant(A), ajouter i à la case (A, $).
2. Marquer erreur dans toutes les cases restées vides.

d’autres grammaires, non ambigües, dont on a pu montrer qu’aucun regard en avant de


taille bornée ne permettra une analyse descendante déterministe.
Encore un point à noter : les grammaires LL(k) forment des familles imbriquées strictement
les unes dans les autres, pour tout k.

16

Vous aimerez peut-être aussi