From c4d98bd981a3a204b495bf3071f4b5728814a4b5 Mon Sep 17 00:00:00 2001 From: "David A. Madore" Date: Tue, 7 Nov 2017 16:21:08 +0100 Subject: Expand discussion about practical parsing of CFGs (LL and LR approaches). --- notes-inf105.tex | 142 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 45 deletions(-) diff --git a/notes-inf105.tex b/notes-inf105.tex index 470a46c..818b58b 100644 --- a/notes-inf105.tex +++ b/notes-inf105.tex @@ -4820,8 +4820,8 @@ langage $L(G)$ que la précédente (elle est donc faiblement équivalente inambiguë (cf. \ref{ambiguous-grammar}), il ne peut pas être analysé par un analyseur LL (cf. \ref{handwaving-on-ll-and-lr}).)\par} -\thingy Sur l'alphabet $\Sigma = \{a,b\}$, considérons la grammaire -(d'axiome $S$) +\thingy\label{example-unambiguous-but-not-deterministic-grammar} Sur +l'alphabet $\Sigma = \{a,b\}$, considérons la grammaire (d'axiome $S$) \[ \begin{aligned} S &\rightarrow U \;|\; V \;|\; \varepsilon\\ @@ -4830,11 +4830,12 @@ V &\rightarrow aVbb \;|\; abb\\ \end{aligned} \] Il est clair que le langage $L(G,U)$ des mots qui dérivent de $U$ est -$\{a^i b^i : i>0\}$ comme en \ref{basic-example-context-free-grammar}, -et de façon analogue, le langage $L(G,V)$ des mots qui dérivent de $V$ -est $\{a^i b^{2i} : i>0\}$. Le langage $L(G) = L(G,U) \cup L(G,V)$ -est donc l'ensemble des mots de la forme $a^i b^j$ où $j=i$ \emph{ou - bien} $j=2i$. +$\{a^i b^i : i\geq 1\}$ comme +en \ref{basic-example-context-free-grammar}, et de façon analogue, le +langage $L(G,V)$ des mots qui dérivent de $V$ est $\{a^i b^{2i} : +i\geq 1\}$. Le langage $L(G) = L(G,U) \cup L(G,V)$ est donc +l'ensemble des mots de la forme $a^i b^j$ où $i\geq 1$ et $j=i$ +\emph{ou bien} $j=2i$. {\footnotesize (Ce langage est intéressant sur le plan théorique, car bien qu'il soit algébrique et défini par une grammaire inambiguë @@ -5461,6 +5462,8 @@ les automates à pile déterministes (qu'il faut définir soigneusement) acceptent strictement moins de langages que les automates à pile non déterministes. +\bigbreak + \thingy Une approche simple, quoique terriblement inefficace, pour résoudre algorithmiquement, \emph{en théorie}, le problème de décider si un mot $w$ appartient au langage $L(G)$ engendré par une grammaire @@ -5649,49 +5652,93 @@ cubique en la longueur de $w$. \par} -\thingy\label{handwaving-on-ll-and-lr} Du point de vue pratique, il existe deux approches principales -pour analyser les langages définis par des grammaires hors contexte -(supposées \emph{au minimum} inambiguës) : +\bigbreak + +\thingy\label{handwaving-on-ll-and-lr} Du point de vue pratique, on ne +cherche pas simplement à savoir si un mot appartient au langage +engendré par une grammaire, mais aussi à en construire un arbre +d'analyse ; par ailleurs, la complexité algorithmique des approches +décrites en \ref{algebraic-languages-are-decidable} et meme +en \ref{handwaving-on-dynamical-programming} est inacceptable. En +contrepartie de ces exigences, on est prêt à accepter de mettre des +contraintes sur la grammaire qui la rendent plus facile à analyser : +\emph{au minimum}, on supposera que la grammaire est inambiguë +(cf. \ref{ambiguous-grammar}), et en fait, on imposera des contraintes +beaucoup plus fortes (par exemple, la grammaire présentée +en \ref{example-unambiguous-but-not-deterministic-grammar}, bien +qu'inambiguë, ne sera pas acceptable car il n'y a pas de manière +déterministe de l'analyser, ni même d'analyser le langage qu'elle +engendre) ; ces contraintes sont assez techniques et difficiles à +décrire : dans la pratique, elles consistent essentiellement à essayer +de fabriquer l'analyseur et à constater si l'algorithme échoue. + +Il existe deux principales approches pour construire un analyseur pour +une grammaire hors contexte (sujette à diverses contraintes +supplémentaires) ; dans les deux cas, on construit une sorte +d'automate à pile (cf. \ref{handwaving-on-stack-automata}) +déterministe, mais l'utilisation de la pile est très différente dans +les deux cas. De façon très simplifiée : \begin{itemize} -\item Les analyseurs \defin[LL (analyse)]{LL} procèdent de façon \emph{descendante} (en - anglais « top-down »), parcourent le mot depuis la gauche (« L ») et - génèrent la dérivation gauche (« L ») de l'arbre d'analyse fabriqué, - en partant de la racine et en descendant jusqu'aux - feuilles\footnote{En botanique, un arbre a la racine en bas et les - feuilles en haut ; en informatique, on les représente plutôt - racine en haut et feuilles en bas.}. -\item Les analyseurs \defin[LR (analyse)]{LR} procèdent de façon \emph{ascendante} (en - anglais « bottom-up »), parcourent le mot depuis la gauche (« L ») - et génèrent la dérivation droite (« R ») de l'arbre d'analyse - fabriqué, en partant des feuilles et en remontant jusqu'aux racines. +\item Les analyseurs \defin[LL (analyse)]{LL} procèdent de façon + \emph{descendante} (en anglais « top-down »), parcourent le mot + depuis la gauche (« L ») et génèrent la dérivation gauche (« L ») de + l'arbre d'analyse fabriqué, en partant de la racine et en descendant + jusqu'aux feuilles\footnote{En botanique, un arbre a la racine en + bas et les feuilles en haut ; en informatique, on les représente + plutôt racine en haut et feuilles en bas.}. La pile de + l'analyseur LL sert, intuitivement, à mémoriser les règles qu'on a + commencé à reconnaître, en partant de l'axiome de la grammaire, et + qui restent encore à compléter. +\item Les analyseurs \defin[LR (analyse)]{LR} procèdent de façon + \emph{ascendante} (en anglais « bottom-up »), parcourent le mot + depuis la gauche (« L ») et génèrent la dérivation droite (« R ») de + l'arbre d'analyse fabriqué, en partant des feuilles et en remontant + jusqu'aux racines. La pile de l'analyseur LR sert, intuitivement, à + mémoriser les fragments d'arbre déjà construit, en partant des + feuilles, et qui restent encore à regrouper. \end{itemize} +On peut écrire un analyseur LL ou (plus difficilement) LR à la main +dans un cas simple, mais en général ces analyseurs sont fabriqués par +des algorithmes systématiques, implémentés dans des programmes tels +que YACC ou Bison (qui produit des analyseurs LR, même si Bison peut +dépasser ce cadre) ou JavaCC (qui produit des analyseurs LL). + L'idée générale à retenir est que les analyseurs LR sont strictement plus puissants que les analyseurs LL (ils sont capables d'analyser strictement plus de grammaires, cf. \ref{example-lr-non-ll-grammar}), -mais leur écriture est plus difficile et les messages d'erreur qu'ils -retournent sont plus difficiles à comprendre. +mais leur génération est plus difficile et les messages d'erreur +qu'ils retournent en cas de problème de syntaxe sont plus difficiles à +comprendre pour l'utilisateur. -\thingy Pour illustrer ces différences, considérons une grammaire très -simple à analyser comme +\thingy\label{example-ll-and-lr-analysis} Pour illustrer le +fonctionnement et les différences des analyseurs LL et LR, considérons +une grammaire très simple à analyser comme \[ \begin{aligned} S &\rightarrow TS \;|\; c\\ T &\rightarrow aSb\\ \end{aligned} \] +(il s'agit d'une variante de celle considérée +en \ref{example-well-parenthesized-expressions}, où on a ajouté un $c$ +pour rendre l'analyse plus simple en évitant les +productions $\varepsilon$ ; on peut imaginer, si on veut, qu'il s'agit +d'une forme extrêmement primitive de XML où $a$ représente une balise +ouvrante, $b$ une balise fermante, et $c$ une balise vide). L'approche la plus évidente, si on doit écrire une fonction « analyser -un mot comme dérivant de $S$ dans cette grammaire » consiste à faire +un mot comme dérivant de $S$ dans cette grammaire » consiste à coder deux fonctions mutuellement récursives, « chercher un préfixe qui dérive de $S$ » et « chercher un préfixe qui dérive de $T$ ». En observant que tout mot qui dérive de $T$ doit commencer par la lettre $a$, ce qui permet de distinguer les mots dérivant des règles $S\rightarrow TS$ et $S\rightarrow c$, on va écrire : \begin{itemize} -\item Fonction « rechercher un préfixe qui dérive de $S$ » (prenant en - entrée un mot $w\in\{a,b\}^*$, et renvoyant un préfixe de $w$ et un - arbre de dérivation de $w$ à partir de $S$) : +\item La fonction « rechercher un préfixe qui dérive de $S$ » (prenant + en entrée un mot $w\in\{a,b\}^*$, et renvoyant un préfixe de $w$ et + un arbre de dérivation de $w$ à partir de $S$) est définie comme + suit : \begin{itemize} \item si la première lettre de $w$ est $c$, renvoyer le préfixe $c$ et l'arbre trivial $S\to c$, sinon : @@ -5709,9 +5756,10 @@ $S\rightarrow TS$ et $S\rightarrow c$, on va écrire : racines de sous-arbres donnés par $\mathscr{U}$ et $\mathscr{V}$ respectivement). \end{itemize} -\item Fonction « rechercher un préfixe qui dérive de $T$ » (prenant en - entrée un mot $u\in\{a,b\}^*$, et renvoyant un préfixe de $u$ et un - arbre de dérivation de $u$ à partir de $T$) : +\item La fonction « rechercher un préfixe qui dérive de $T$ » (prenant + en entrée un mot $u\in\{a,b\}^*$, et renvoyant un préfixe de $u$ et + un arbre de dérivation de $u$ à partir de $T$) est définie comme + suit : \begin{itemize} \item vérifier que la première lettre est un $a$ (sinon, soulever une exception indiquant une erreur d'analyse), @@ -5733,18 +5781,22 @@ $S\rightarrow TS$ et $S\rightarrow c$, on va écrire : Cette approche réussit sur cette grammaire très simple (où on peut notamment se convaincre que l'éventuel préfixe dérivant de $S$ ou de $T$ est toujours défini de façon unique). L'analyseur qu'on vient de -décrire s'appelle un « analyseur par descente récursive » ; mais -plutôt qu'utiliser la récursivité du langage de programmation -(c'est-à-dire la pile système), on peut aussi utiliser une pile comme -structure de données, et on obtient ainsi essentiellement un automate -à pile, qui utilise sa pile pour retenir les règles qu'il a commencé à -analyser (à partir de la racine de l'arbre d'analyse en cours de -construction). On a essentiellement construit un analyseur LL, ou -plus exactement LL($1$) (le « $1$ » indiquant qu'on se contente de -lire une unique lettre du mot pour décider quelle règle chercher à -analyser), pour ce langage. C'est ici l'approche « descendante » : -l'arbre se construit à partir de la racine et la pile sert à retenir -les règles qu'on a commencé à reconnaître. +décrire s'appelle un « analyseur par descente récursive ». Or, plutôt +qu'utiliser la récursivité du langage de programmation (c'est-à-dire +la pile système), on peut aussi utiliser une pile comme structure de +données, et transformer les fonctions récursives en fonctions +itératives\footnote{Ceci est un fait général : tout ensemble de + fonctions récursives peut se réécrire pour utiliser une pile comme + structure de données explicite à la place de la pile système.}. On +obtient ainsi essentiellement un automate à pile, qui utilise sa pile +pour retenir les règles qu'il a commencé à analyser (à partir de la +racine de l'arbre d'analyse en cours de construction). On a +essentiellement construit un analyseur LL, ou plus exactement LL($1$) +(le « $1$ » indiquant qu'on se contente de lire une unique lettre du +mot pour décider quelle règle chercher à analyser), pour ce langage. +C'est ici l'approche « descendante » : l'arbre se construit à partir +de la racine et la pile sert à retenir les règles qu'on a commencé à +reconnaître. \smallbreak @@ -5774,7 +5826,7 @@ tel analyseur, mais sur cet exemple précis il est facile de se convaincre qu'il fonctionne et de comprendre pourquoi : il s'agit essentiellement là d'un analyseur LR (en fait, LR($0$), le « $0$ » indiquant qu'on n'a jamais eu besoin de regarder au-delà du symbole -courant pour décider quoi faire), C'est ici l'approche +courant pour décider quoi faire). C'est ici l'approche « ascendante » : l'arbre se construit à partir des feuilles et la pile sert à retenir les nonterminaux au sommet des morceaux d'arbre déjà construits (et éventuellement les arbres eux-mêmes). -- cgit v1.2.3