Algorithmique & Programmation (INF 431) - Analyse syntaxique ...
Algorithmique & Programmation (INF 431) - Analyse syntaxique ...
Algorithmique & Programmation (INF 431) - Analyse syntaxique ...
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
<strong>Algorithmique</strong> & <strong>Programmation</strong><br />
(<strong>INF</strong> <strong>431</strong>)<br />
<strong>Analyse</strong> <strong>syntaxique</strong> récursive descendante<br />
Benjamin Werner François Pottier<br />
22 mai 2013
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
La semaine dernière, nous avons présenté :<br />
• la notion de grammaire algébrique ;<br />
• des algorithmes de reconnaissance non directionnels<br />
et non déterministes.<br />
Résumé<br />
Ces algorithmes sont coûteux : O(n 2 ) en espace et O(n 3 ) en temps.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Peut-on avoir autant pour moins cher ?<br />
Peut-on espérer obtenir mieux que O(n 3 ) ?<br />
Valiant a donné en 1975 une variante de l’algorithme de Cocke, Younger et<br />
Kasami dont la complexité est celle de la multiplication de matrices<br />
booléennes, soit O(n 2,... ).<br />
Malheureusement, cela reste trop cher.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
D’où provient ce coût ?<br />
Les algorithmes que nous avons présentés étudient de nombreux<br />
sous-problèmes.<br />
Face au choix d’une production A → β et d’un indice de coupure j,<br />
ils étudient toutes les possibilités :<br />
<br />
x(Aα)ik =<br />
xβij ∧ xαjk<br />
A→β∈P i≤j≤k<br />
Ce non-déterminisme coûte très cher, en temps et en espace (toute l’entrée<br />
et tous les x(Aα)ik sont stockés en mémoire).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Peut-on avoir moins pour moins cher ?<br />
Ne stockons plus l’entrée ni les résultats intermédiaires.<br />
Revenons à un algorithme récursif naïf et tentons de le rendre :<br />
• directionnel – une « tête de lecture » évoluera de gauche à droite ;<br />
• déterministe – face à un choix, on devra déterminer immédiatement<br />
quelle possibilité est « la bonne » !<br />
Bien sûr, cela ne sera pas toujours possible : cette approche ne sera<br />
applicable qu’à certaines grammaires.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Vers un algorithme directionnel<br />
Dans un premier temps, reformulons notre mise en équations pour tenter<br />
d’introduire l’idée de directionnalité.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Vers un algorithme directionnel<br />
Nous avons jusqu’ici raisonné en termes de sous-problèmes booléens :<br />
« Étant donnés α, i et k, la phrase α engendre-t-elle input [i,k) ? »<br />
Nous pourrions poser une question qui appelle une réponse ensembliste :<br />
« Étant donnés α et i, quels sont les indices k tels que α engendre<br />
input [i,k) ? »<br />
Cela suggère cette interprétation :<br />
« Si la tête de lecture est en position i, et si elle consomme un mot<br />
engendré par α, quelles positions k peut-elle atteindre ? »
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Système d’équations ensemblistes<br />
Écrivons donc un système d’équations dont les inconnues Xαi dénotent des<br />
ensembles d’entiers :<br />
Xɛi = { i }<br />
<br />
Xα(i+1)<br />
∅<br />
<br />
si i < n et a = input i<br />
sinon<br />
{ k | ∃j. j ∈ Xβi ∧ k ∈ Xαj }<br />
X(aα)i =<br />
X(Aα)i =<br />
=<br />
A→β∈P<br />
<br />
<br />
{k}<br />
A→β∈P j∈Xβi k∈Xαj
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
<strong>Analyse</strong> descendante non déterministe<br />
Appliquons naïvement la technique récursive (également appelée<br />
descendante) pour tenter de résoudre ces équations.<br />
On écrit une famille de fonctions consumeα qui :<br />
• attendent la position i de la tête de lecture ;<br />
• renvoient l’ensemble des positions k que la tête de lecture peut<br />
atteindre après avoir consommé un mot de L(α).<br />
Les cas où α est un symbole a ou A suffisent.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
<strong>Analyse</strong> descendante non déterministe<br />
Pour mieux fixer les idées, voyons cela en Java.<br />
On se donne une classe Terminal dotée d’une méthode equals.<br />
On suppose donnée l’entrée :<br />
final Vector < Terminal > input ;<br />
On représente les ensembles d’entiers par des listes dont on ne cherche<br />
pas à éliminer les éventuels doublons.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
<strong>Analyse</strong> descendante non déterministe<br />
La fonction consumea associée à un terminal a renvoie soit l’ensemble vide,<br />
qui représente un échec, soit un singleton, qui représente une réussite :<br />
// This method recognizes the terminal symbol a.<br />
LinkedList < Integer > consumeTerminal ( Terminal a, int i)<br />
{<br />
LinkedList < Integer > results =<br />
new LinkedList < Integer > () ;<br />
if (i < input . size () && a. equals ( input . get (i )))<br />
results . add (i+1) ;<br />
return results ;<br />
}<br />
Un échec est une liste (vide) de réussites !
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
<strong>Analyse</strong> descendante non déterministe<br />
Voici consumeE pour la grammaire d’expressions arithmétiques.<br />
LinkedList < Integer > consumeExpression ( int i)<br />
{<br />
LinkedList < Integer > results =<br />
new LinkedList < Integer > () ;<br />
}<br />
// Try E -> E + E.<br />
for ( int j : consumeExpression (i))<br />
for ( int k : consumeTerminal ( Terminal .PLUS , j))<br />
for ( int l : consumeExpression (k))<br />
results . add (l) ;<br />
// Try every other production in the same way ,<br />
// adding more and more results to the set .<br />
return results ;<br />
Le code est une traduction directe de la grammaire.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Que penser de ce code ?<br />
Cet analyseur descendant naïf est simple mais très inefficace.<br />
Si la grammaire est récursive à gauche, il ne termine pas. C’est le cas ici !<br />
Même lorsqu’il termine, il peut avoir un coût exponentiel, à cause du<br />
non-déterminisme.<br />
Il n’est pas directionnel, à nouveau à cause du non-déterminisme.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Comment l’améliorer ?<br />
On souhaiterait conserver la simplicité de cette approche mais éliminer le<br />
non-déterminisme.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Et si un oracle nous aidait ?<br />
Si, au moment où se présentent n possibilités, un « oracle » pouvait garantir<br />
que n − 1 d’entre elles vont échouer, alors aucun choix ne serait nécessaire.<br />
On étudierait la dernière possibilité (qui peut encore échouer ou réussir).<br />
On obtiendrait un analyseur simple, déterministe, directionnel.<br />
On peut espérer qu’il termine toujours et soit efficace.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Et si un oracle nous aidait ?<br />
Concrètement, c’est la fonction consumeA qui va faire appel à l’oracle.<br />
Parmi toutes les productions A → β, l’oracle doit désigner « la bonne » : la<br />
seule qui a une chance de réussir.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Bien sûr, cela n’est pas possible en général.<br />
Est-ce possible ?<br />
D’abord, si un tel oracle existe, alors nous avons un algorithme d’analyse<br />
déterministe, donc, pour toute entrée input, il existe au plus un arbre de<br />
production.<br />
Donc, la grammaire doit être non ambiguë.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Est-ce possible ?<br />
Ensuite, on souhaite que l’oracle soit un dispositif simple, efficace, qui n’a<br />
accès qu’à très peu d’information.<br />
Ceci va restreindre encore la classe des grammaires pour lesquelles un<br />
oracle existe.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Qu’utilise l’oracle ?<br />
À quelles informations l’oracle doit-il ou peut-il avoir accès ?<br />
• certainement au non-terminal A dont on doit choisir l’une des<br />
productions ;<br />
• certainement à une partie de l’entrée au-delà de la tête de lecture.<br />
Le minimum est d’autoriser l’oracle à consulter le premier symbole de<br />
l’entrée au-delà de la tête de lecture (# si la fin de l’entrée est atteinte).<br />
Un oracle est donc une fonction qui à A et a associe (au plus) une<br />
production A → β.<br />
On peut le représenter par un tableau à deux dimensions.<br />
Un analyseur basé sur un tel oracle est appelé LL(1).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Existence d’un oracle<br />
Pouvons-nous construire un oracle pour notre petite grammaire des<br />
expressions arithmétiques ?<br />
E → E + E<br />
E → E - E<br />
E → E * E<br />
E → E / E<br />
E → ( E )<br />
E → int<br />
Non : cette grammaire est ambiguë, donc n’appartient pas à la classe<br />
LL(1).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Existence d’un oracle<br />
Pouvons-nous construire un oracle pour la version reformulée et non<br />
ambiguë ?<br />
E → E + T T → T / F<br />
E → E - T T → F<br />
E → T F → ( E )<br />
T → T * F F → int<br />
Non : elle n’appartient pas non plus à la classe LL(1). Pourquoi ?<br />
Il y a intuitivement deux raisons à cela...
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Cette grammaire est récursive à gauche.<br />
Récursivité à gauche<br />
Si à (E, a) l’oracle associe E → E + T, l’analyseur ne terminera pas.<br />
Mais si l’oracle ne propose jamais E → E + T, alors certains mots valides<br />
ne seront jamais reconnus.<br />
Une grammaire récursive à gauche, où E → + Eα, n’est pas LL(1).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Cette grammaire présente des facteurs à gauche.<br />
Facteurs à gauche<br />
Pour choisir entre E → E + T et E → E - T, l’oracle devrait avoir accès au<br />
symbole + ou - , qui n’est pas en général le premier symbole de l’entrée.<br />
Une grammaire qui présente un facteur non trivial à gauche, c’est-à-dire<br />
deux productions A → βγ1 et A → βγ2 où L(β) {ɛ}, n’est pas LL(1).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Élimination de la récursivité à gauche<br />
Pouvons-nous proposer une grammaire :<br />
• équivalente à la précédente – elle engendre le même langage ;<br />
• qui ne présente ni facteurs à gauche ni récursivité à gauche ?
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Nous avons :<br />
Élimination de la récursivité à gauche<br />
E → E + T<br />
E → E - T<br />
E → T<br />
Une expression E est donc « une liste de T, séparés par des + ou - . »<br />
En d’autres termes, c’est « un T, suivi d’un certain nombre de +T ou - T ».<br />
Si l’on s’autorise la répétition et le choix (notation « EBNF »), on peut écrire :<br />
E → T ( + T | - T ) ⋆<br />
On peut aussi écrire, sous forme ordinaire :<br />
E → T E ′<br />
E ′ → + T E ′ | - T E ′ | ɛ
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Élimination de la récursivité à gauche<br />
En appliquant la même idée à T, on obtient cette nouvelle grammaire :<br />
E → T E ′ T → F T ′<br />
E ′ → + T E ′ T ′ → * F T ′<br />
E ′ → - T E ′ T ′ → / F T ′<br />
E ′ → ɛ T ′ → ɛ<br />
F → int F → ( E )<br />
qui n’est pas récursive à gauche et n’a pas de facteurs à gauche.<br />
Il est toujours possible de transformer une grammaire, sans modifier le<br />
langage engendré, pour éliminer récursivité à gauche et facteurs à gauche.<br />
Travail pénible si effectué manuellement !
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Existence d’un oracle<br />
Existe-t-il un oracle pour cette nouvelle grammaire ?<br />
La réponse est oui, comme nous allons le montrer.<br />
Ainsi, même si deux grammaires sont équivalentes, il se peut que l’une<br />
admette un oracle, donc appartienne à la classe LL(1), et l’autre pas.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Construisons un oracle<br />
L’existence d’un oracle n’est pas aussi immédiate qu’on pourrait le croire.<br />
Sachant que le prochain symbole de l’entrée est a, comment choisir entre<br />
les trois productions associés à E ′ ?<br />
E ′ → + T E ′<br />
E ′ → - T E ′<br />
E ′ → ɛ<br />
Si a { +, - }, alors il est évident que seule E ′ → ɛ peut réussir.<br />
Mais si a est + (par exemple), que dire ?
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Construisons un oracle<br />
Si le prochain symbole de l’entrée est +, on voudrait annoncer que seule<br />
E ′ → + T E ′ peut réussir.<br />
Il faut donc se convaincre que E ′ → ɛ doit échouer.<br />
Pour cela, il faut vérifier que « + ne peut pas suivre E ′ », c’est-à-dire que le<br />
symbole de départ n’engendre aucune phrase de la forme αE ′ + β.<br />
On se convainc que E ′ ne peut être suivi que de ) ou #.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Construisons un oracle<br />
On étudie de même le cas des symboles T ′ et F, et on obtient une table :<br />
E ′ T ′ F<br />
int F → int<br />
( F → ( E )<br />
) E ′ → ɛ T ′ → ɛ<br />
+ E ′ → + T E ′ T ′ → ɛ<br />
- E ′ → - T E ′ T ′ → ɛ<br />
* T ′ → * F T ′<br />
/ T ′ → / F T ′<br />
# E ′ → ɛ T ′ → ɛ<br />
L’oracle répond en temps constant par simple consultation de cette table.<br />
Les cases vides de la table indiquent une erreur : l’entrée est incorrecte.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Nous avons construit un oracle<br />
La dernière version de notre grammaire appartient donc à la classe LL(1).<br />
Ceci démontre qu’elle est non ambiguë.<br />
La version précédente était donc déjà non ambiguë, puisqu’équivalente à<br />
celle-ci, mais n’appartenait pas à la classe LL(1).<br />
Toute grammaire LL(1) est non ambiguë ; la réciproque est fausse.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Les choses se simplifient<br />
Le code de l’analyseur non déterministe naïf se simplifie en deux temps.<br />
D’abord, il devient déterministe, donc chaque fonction renvoie une seule<br />
position finale (ou bien échoue via une exception).<br />
Ensuite, au lieu de demander une position initiale et renvoyer une position<br />
finale, il suffit de maintenir la position courante dans une variable globale.<br />
// This is the position of the read head .<br />
int i ;<br />
C’est possible car il n’y a plus de retour en arrière (« backtracking »).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Consommer un symbole terminal<br />
La fonction consumea avance la tête de lecture ou échoue.<br />
// This method recognizes the terminal symbol a.<br />
void consumeTerminal ( Terminal a)<br />
{<br />
if (i < input . size () && a. equals ( input . get (i )))<br />
i++ ;<br />
else<br />
throw new Error (" Syntax error at " + i) ;<br />
}
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Consulter le prochain symbole terminal<br />
La fonction peek, utilisée par l’oracle, consulte le prochain symbole sans<br />
avancer la tête de lecture.<br />
Terminal peek ()<br />
{<br />
return i< input . size () ? input . get (i) : Terminal . EOS ;<br />
}<br />
Le symbole spécial Terminal.EOS représente #.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Consulter l’oracle et agir<br />
L’oracle prend la forme d’un simple switch. Voici la fonction consumeE ′ :<br />
void consumeExpressionTail ()<br />
{<br />
switch ( peek ()) { // Examine the next input symbol .<br />
case PLUS : // E ’ -> + T E ’<br />
consumeTerminal ( Terminal . PLUS) ;<br />
consumeTerm() ;<br />
consumeExpressionTail() ;<br />
break ;<br />
case MINUS : // E ’ -> - T E ’<br />
consumeTerminal ( Terminal . MINUS) ;<br />
consumeTerm() ;<br />
consumeExpressionTail() ;<br />
break ;<br />
case RPAR :<br />
case EOS : // E ’ -> epsilon<br />
break ; // Nothing is consumed .<br />
default : // Error .<br />
throw new Error (" Syntax error at " + i) ;<br />
}<br />
}
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Agir sans consulter l’oracle<br />
Lorsqu’il n’y a qu’une production, inutile de consulter l’oracle.<br />
Voici la fonction consumeE :<br />
void consumeExpression ()<br />
{<br />
// E -> T E ’<br />
consumeTerm() ;<br />
consumeExpressionTail() ;<br />
}
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
L’algorithme est directionnel<br />
La tête de lecture évolue uniquement de gauche à droite.<br />
On peut donc modifier le code pour représenter l’entrée non pas par un<br />
tableau mais par un flot (« stream ») de symboles.<br />
interface TerminalStream {<br />
}<br />
// Returns the next symbol without consuming it .<br />
// Returns EOS if the end has been reached .<br />
Terminal peek () ;<br />
// Discards the next symbol , if there is one .<br />
// Throws Error otherwise .<br />
void consume () ;<br />
L’espace occupé par l’entrée est alors O(1).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Terminaison<br />
Si consumeA a appelé (directement ou indirectement) consumeB sans que<br />
la tête de lecture ait avancé, alors on a A → + Bγ pour un certain γ.<br />
Si l’algorithme ne termine pas, alors consumeA a appelé consumeA sans<br />
avancer, pour un certain A. Donc, on a A → + Aγ pour un certain γ : la<br />
grammaire est récursive à gauche.<br />
Ceci contredit l’hypothèse que la grammaire est LL(1).<br />
Donc, l’algorithme termine toujours.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Complexité<br />
Mieux, le nombre d’appels de fonctions effectués sans que la tête de<br />
lecture avance est borné par le nombre de symboles non-terminaux.<br />
Il en découle que la complexité de l’algorithme est O(n) en temps et en<br />
espace.<br />
• ne pas oublier l’espace occupé par la pile des appels de fonctions !
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Optimisation des appels terminaux<br />
Les appels à consumeExpressionTail sont terminaux. Cette fonction peut<br />
être remplacée par une boucle :<br />
void consumeExpression ()<br />
{<br />
// E -> T E ’<br />
consumeTerm() ;<br />
// consumeExpressionTail :<br />
while ( true ) {<br />
switch ( peek ()) {<br />
case PLUS : // E ’ -> + T E ’<br />
consumeTerminal ( Terminal . PLUS) ;<br />
consumeTerm() ;<br />
break ; // Continue looping .<br />
case RPAR :<br />
case EOS : // E ’ -> epsilon<br />
return ; // Exit the loop .<br />
...<br />
}<br />
}<br />
}<br />
Dans le cas de Java, cela diminue l’espace utilisé sur la pile.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Construire un arbre<br />
Jusqu’ici, j’ai parlé de reconnaissance : répondre par « oui » ou « non ».<br />
Pour l’analyse <strong>syntaxique</strong>, il faut de plus construire un arbre :<br />
• soit un arbre de production,<br />
• soit directement un arbre de syntaxe abstraite.<br />
La fonction consumeA renvoie alors non pas void mais un objet, parfois<br />
appelé « valeur sémantique ».
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Construire un arbre<br />
Par exemple, consumeExpression construit et renvoie une expression :<br />
Expression consumeExpression ()<br />
{<br />
// E -> T E ’<br />
Expression head = consumeTerm() ;<br />
return consumeExpressionTail ( head) ;<br />
}<br />
On décide de passer head à consumeExpressionTail pour qu’elle puisse<br />
construire un arbre au-dessus.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Construire un arbre<br />
consumeExpressionTail attend une expression left, reconnaît une partie<br />
de l’entrée conforme à E ′ , et construit une expression plus complexe :<br />
Expression consumeExpressionTail ( Expression left )<br />
{<br />
switch ( peek ()) {<br />
case MINUS : // E ’ -> - T E ’<br />
consumeTerminal ( Terminal . PLUS) ;<br />
Expression right = consumeTerm() ;<br />
// Subtraction is left - associative !<br />
left = new ESubtraction (left , right) ;<br />
return consumeExpressionTail ( left) ;<br />
case RPAR :<br />
case EOS : // E ’ -> epsilon<br />
return left ; // Nothing is recognized .<br />
...<br />
}<br />
}<br />
Exercice : combiner ceci avec l’optimisation des appels terminaux.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Apercevant a, faut-il essayer A → β ?<br />
Comment construit-on l’oracle dans le cas général ?<br />
On doit savoir répondre, pour une production A → β et un symbole<br />
terminal a, à la question :<br />
« si le prochain symbole d’entrée est a, faut-il essayer A → β ? »<br />
Si pour plusieurs productions A → β la réponse est « oui », alors la<br />
construction échoue : il n’existe pas d’oracle.<br />
Sinon, on peut construire une table qui à chaque paire (A, a) associe soit<br />
une production A → β soit « erreur » : c’est l’oracle.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Apercevant a, faut-il essayer A → β ?<br />
Pour répondre à cette question, on se demande :<br />
« β engendre-t-il un mot qui débute par a ? »<br />
Si oui, on répond : « oui, il faut essayer ».<br />
Si non, on se demande :<br />
« β engendre-t-il ɛ et a peut-il suivre A ? »<br />
Si oui, on répond : « oui, il faut essayer », sinon, « non, échec garanti ».
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Questions à propos de la grammaire<br />
Il faut donc savoir répondre à trois types de questions :<br />
• « β engendre-t-il ɛ ? »<br />
• « β engendre-t-il un mot qui débute par a ? »<br />
• « a peut-il suivre A ? »<br />
Fort heureusement, ces informations sont données par la plus petite<br />
solution de certains systèmes d’équations booléennes (encore !).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Écrivons nullable(α) ssi α engendre ɛ. Alors :<br />
nullable(ɛ) = vrai<br />
Caractérisation de nullable<br />
nullable(aα) = faux<br />
⎛<br />
⎞<br />
<br />
nullable(Aα) = ⎜⎝<br />
nullable(β) ⎟⎠<br />
∧ nullable(α)<br />
A→β∈P<br />
et les nullable(α) sont la plus petite solution de ces équations.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Caractérisation de first<br />
Écrivons a ∈ first(α) ssi α engendre une phrase de la forme aβ. Alors :<br />
a ∈ first(ɛ) = faux<br />
a ∈ first(bα) = a = b<br />
⎛<br />
⎞<br />
<br />
a ∈ first(Aα) = ⎜⎝<br />
a ∈ first(β) ⎟⎠<br />
∨ (nullable(A) ∧ a ∈ first(α))<br />
A→β∈P<br />
et c’est à nouveau la plus petite solution qui nous intéresse.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Caractérisation de follow<br />
On ajoute à la grammaire G la production S ′ → S#.<br />
Écrivons a ∈ follow(B) ssi S ′ engendre une phrase de la forme αBaβ.<br />
Ici, a appartient à Σ ∪ {#}. On a :<br />
a ∈ follow(B) =<br />
⎛<br />
⎜⎝<br />
<br />
⎞<br />
a ∈ first(β) ∨ (nullable(β) ∧ a ∈ follow(A)) ⎟⎠<br />
A→αBβ∈P<br />
et c’est à nouveau la plus petite solution qui nous intéresse.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
En résumé<br />
Déterminer si G appartient à la classe LL(1), et si oui construire l’oracle, se<br />
fait en temps O(| G | . | Σ |).
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
Ce que nous avons vu<br />
Il existe une hiérarchie de formalismes plus ou moins expressifs.<br />
Plus le formalisme est expressif, plus la reconnaissance est coûteuse :<br />
• expressions régulières : espace O(1), temps O(n) ;<br />
• grammaires LL(1) : espace O(n), temps O(n) ;<br />
• grammaires algébriques arbitraires : espace O(n 2 ), temps O(n 3 ).<br />
Le choix d’un formalisme est donc important et demande un compromis<br />
entre expressivité et coût.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
D’autres échelons de cette hiérarchie.<br />
Ce que nous n’avons pas vu<br />
• la classe LR(1) : espace et temps O(n) ; contient strictement LL(1),<br />
autorise la récursivité et les facteurs à gauche. (Voir <strong>INF</strong>564 !)<br />
D’autres algorithmes d’analyse <strong>syntaxique</strong>.<br />
• Earley : non déterministe, directionnel, « adaptatif » :<br />
• O(n) si la grammaire est LR(1),<br />
• O(n 2 ) si elle est non ambiguë,<br />
• O(n 3 ) dans le cas général.<br />
Les outils qui transforment une grammaire en un analyseur.<br />
• JavaCUP, ANTLR, JavaCC, et d’autres encore (clic !) ;<br />
• voir le code de CalculiX pour quelques exemples.
<strong>INF</strong> <strong>431</strong><br />
Benjamin<br />
Werner,<br />
François<br />
Pottier<br />
Descente<br />
naïve<br />
Descente<br />
déterministe<br />
LL(1)<br />
Oracle<br />
Exemple<br />
Java<br />
Cas général<br />
Conclusion<br />
• L’analyse <strong>syntaxique</strong> est partout !<br />
• décoder un fichier, décoder un message, . . .<br />
Que retenir ?<br />
• Écrivez la grammaire, vous obtiendrez un analyseur.<br />
• utiliser un outil existant<br />
• ne pas réinventer la roue !<br />
• Certains formalismes sont plus expressifs que d’autres.<br />
• expressions régulières < LL(1) < LR(1) < grammaires arbitraires < . . .<br />
• choisir un formalisme adapté