04.07.2013 Views

Algorithmique & Programmation (INF 431) - Analyse syntaxique ...

Algorithmique & Programmation (INF 431) - Analyse syntaxique ...

Algorithmique & Programmation (INF 431) - Analyse syntaxique ...

SHOW MORE
SHOW LESS

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é

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!