Analyse Syntaxique descendante
- Rappels sur les grammaires
- Définition d'analyse descendante
- Un exemple simple en Ocaml
- Définition et construction de NULLABLE, FIRST, FOLLOW
- Construction de la table LL(1)
- Transformations de grammaires:
- desambigüation par précédence d'opérateurs
- derecursivisation gauche,
- factorisation a gauche
- Un exemple complet
- S
- est un alphabet fini de
terminaux'',<BR></FONT> <DT><FONT SIZE=4> </FONT><FONT SIZE=4><I>V</I></FONT><SUB><FONT SIZE=3><I>N</I></FONT></SUB><FONT SIZE=4> </FONT><DD><FONT SIZE=4> est un ensemble fini de symboles
non terminaux'', avec SÇ VN = Ø
On écrit V pour SÈ VN.
- S
- est un non terminal distingue nomme le
symbole de départ'' (
start symbol'')
- P
- est une relation finie sur VN+× V*
qui définit les
règles'' (
productions'') de la grammaire.
Une règle (a,b)Î P s'écrit aussi a®b.
- type 0
- P quelconque
- type 1
- (contextuelles) pour toute règle a®b on a |a|£|b|.
- type 2
- (libres de contexte) règle de la forme A®b.
- type 3
- (rationnelles) règle de la forme A® ab
ou A® a avec a terminal.
N.B.: il existent d'autres systèmes de production, comme les L-systems utilisés en biologie.
Rappels sur les grammaires: III
Si a®b est une règle, on peut alors l'appliquer a n'importe laquelle séquence xa y pour obtenir xb y et on écrit alors
On écrira w Þ* z, s'il existent w1, w2, ... wn, avec
n³ 1, t.q. w=w1Þ w2 ...Þ wn=z.
Le langage engendre par une grammaire G est
L(G) = {w|SÞ |
|
w} |
Résultats fondamentaux:
Lemma 1.1 [Lemme de la pompe]
Soit L un langage libre de contexte. Il existe p et q dépendant de L tel que
pour tout mot z de L dont la longueur |z| excède p, il existe une décomposition z = uvwxy
avec |vwx| £ q, |vx| ¹ 0 et pour tout n, uvnwxny est dans L.
Conséquences:
-
{wcw | w Î (a|b)*} pas libre de contexte
(ex: déclaration avant utilisation des identificateurs) - {anbmcndm | n,m ³ 1} pas libre de contexte
(ex: correspondance entre paramètres formels et actuels d'une procédure) - {anbncn | n,m ³ 0} pas libre de contexte
(ex: ancienne technique pour indiquer les mots soulignes)
Définition 1.2 [Arbre de dérivation] On peut représenter une dérivation AÞw (avec wÎS) comme un arbre ayant pour noeuds internes des non terminaux, pour feuilles des terminaux et des arcs reliant un noeud X a des fils W1,...Wn si il y a une règle X® X1...Xn.
Définition 1.3 [Grammaire ambiguë] Une grammaire G est ambiguë s'il existe un mot wÎ L(G) avec au moins deux arbres de dérivation différents.
Ex: le mot 1+1+1 dans la grammaire
|
De façon équivalente, une grammaire est ambiguë si elle permette
deux dérivations gauches différentes pour un même mot w.
Backus Naur Form (BNF)
La ``forme normale de Backus'' est une extension du formalisme des grammaires
libre de contexte qui n'ajoute pas de pouvoir expressif (on ne définit pas une
classe plus large de langages), mais qui permet des définitions plus concises.
A la structure des règles on ajoute les notations suivantes:
- étoile de Kleene
- : on peut écrire
A® wx * z A ® wBz B ® xB B ® x B ® e - option
- : on peut écrire A® w[x]za la place de
A ® wBz B ® x B ® e - cas
- : on peut écrire A® w1 | w2 ... | wna la place de
A ® w1 ® ·
·
·A ® wn
Analyse descendante Le problème de l'analyse est celui de prendre une grammaire G, et de construire un algorithme capable, pour toute chaîne w de terminaux, de décider si elle appartient ou non a L(G). De plus, on demande de reconstruire un arbre de dérivation pour w dans G en cas de réponse affirmative.
Certains langages permettent de résoudre ce problème a l'aide d'une méthode fort simple, l'analyse dite descendante.
Cette méthode cherche a construire une dérivation gauche directement a partir du symbole initial, ou de façon équivalente, cherche a construire un arbre de dérivation a partir de la racine avec une exploration en profondeur d'abord de gauche a droite.
Pour cela, l'analyseur doit pouvoir décider, en s'aidant éventuellement avec les prochains k symboles en entrée, quelle est la prochaine production dans la dérivation gauche que l'on reconstruit.
Vue la ``distance'' entre le terminal que l'on voit en entrée et la production a choisir, il n'y a pas trop de chance pour que la méthode fonctionne (en particulier toute grammaire ambiguë posera des problèmes), mais si on réussit, alors le langage de la grammaire est dans la classe LL(k) (langages analysables en parcourant l'entrée de gauche (L) a droite et en reconstituant une dérivation gauche (L)).
Exemple OcamlLex Voyons un exemple de grammaire LL(1) et un analyseur construit a la main en Ocaml:
|
exception Parse_Error;; type token = Int | PLUS | Eof;; let descente gettoken = let tok = ref (gettoken()) in let advance () = tok := gettoken() in let eat t = if (!tok = t) then advance() else raise Parse_Error in let check t = if (!tok = t) then () else raise Parse_Error in let rec ntS () = (ntE(); check(Eof)) and ntE () = match !tok with Int -> (eat(Int); ntE'()) | _ -> raise Parse_Error and ntE' () = match !tok with PLUS -> (eat(PLUS);eat(Int); ntE'()) | _ -> () ( transition vide ) in ntS();;Analyseurs LL(1): choisir la production en regardant le prochain token Pour réaliser un analyseur LL(1), il s'agit de remplir une table de la forme( un lexeur bidon vite fait ) let gettoken_of l = let data = ref l in let f () = let t = List.hd !data in data:= List.tl !data; t in f;;
descente (gettoken_of [Int;PLUS;Int;PLUS;Int;Eof]);;
|
en mettant une règle de production Xi®g dans la case
(Xi,tk) si le fait de voir le token tk indique qu'on peut appliquer
la règle de production Xi®g.
S'il n'y a pas de cases avec plus d'une entrée une fois le remplissage fini,
on aura réussi et on pourra implémenter l'analyseur.
NULLABLE, FIRST, FOLLOW
Question: comment remplir les cases?
Réponse: pour cela on a besoin de calculer pour les symboles
de la grammaire trois ensembles:
- NULLABLE(X)
- vrai ssi X peut produire la chaîne vide (e)
- FIRST(X)
- ensemble de symboles terminaux qui peuvent paraître en première position dans une chaîne g dérivable à partir de X
- FOLLOW(X)
- ensemble de symboles terminaux t qui peuvent paraître immédiatement après X dans une dérivation (i.e. il existe une dérivation contenant Xt, ce qui peut arriver aussi si elle contient XYZt et Y et Z sont nullable).
Définition formelle de NULLABLE, FIRST, FOLLOW On peut voir ces ensembles comme des relations, et alors
-
Nullable est la plus petite relation Nu t.q.
Nu = NuÈ{(X,vrai)|X®Î P} Nu = NuÈ{(X,vrai)|X® Y1...YnÎ P, avec (Yi,vrai)Î Nu} - First est la plus petite relation Fi telle que
Fi = FiÈ {(a,a)|aÎS} Fi = FiÈ {(X,a) | X® Y1...YnÎ P, et existe i avec Y1,...Yi-1 nullable et (Yi,a)Î Fi }
Définition formelle de NULLABLE, FIRST, FOLLOW Enfin, Follow est la plus petite relation Fo telle que
|
Construction de la table LL(1) Une fois calcules Nullable(X), First(X) et Follow(X) pour tout symbole, il est facile de calculer aussi Nullable et First sur une séquence de symboles:
- Nullable(X1...Xn)
- est vrai ssi Nullable(Xi) est vrai pour 1£ i£ n;
- First(X1...Xk)
- est First(X1) si X1 n'est pas nullable
- First(X1...Xk)
- est First(X1)È First(X2...Xk) si X1 est nullable
|
- on met Xi®g dans la case (Xi,t) si tÎ First(g)
- on met Xi®g dans la case (Xi,t) si tÎ Follow(Xi) et Nullable(g)
Transformations préliminaires: élimination de l'ambiguïté L'existence d'une grammaire non ambiguë pour un langage donne L n'est pas décidable. Cependant, dans les cas les plus fréquents, on sait éliminer l'ambiguïté assez facilement:
Exemple: on peut réécrire la grammaire (ambiguë)
en imposant une priorité sur les opérateurs et une associativité, comme:
|
qui reconnaît le même langage, mais n'est pas ambiguë.
Transformations préliminaires: derecursivisation
Une grammaire recursive a gauche pose toujours de problèmes pour LL(1):
si on a deux productions E® E a et E® b,
alors il y aura toujours une double entrée dans les cases (E,t) pour tÎ
First(b).
Mais on sait éliminer la récursion a gauche de toute grammaire: toute
production
peut être remplacée par les productions (équivalentes, si X' est un symbole nouveau) :
|
Transformations préliminaires: factorisation gauche
Un autre exemple de problème pour LL(1) apparaît si on a deux productions
E® a E' et E® a E'' qui ont le même préfixe
a: l'analyseur ne saura pas non plus dans ce cas faire le bon choix.
On sait améliorer la situation en ``factorisant'' a gauche le préfixe commun: toute
production
peut être remplacée par les productions (équivalentes, si X' est un symbole nouveau) :
|
Transformations préliminaires: factorisation gauche, II
Voyons un exemple bien connu:
devient
|
N.B.: ce n'est pas LL(1) non plus, mais on sait mieux gérer ce deuxième cas que
le premier (ex. en donnant priorité au cas ``else'')
Un exemple complet
Le langage des expressions arithmétiques
On veut analyser
|
Cette grammaire est ambiguë, donc nous allons appliquer notre technique de desambiguation.
Un exemple complet: desambiguation En imposant que * lie plus que + et qu'on associe a gauche, on obtient la grammaire non ambiguë:
|
Mais cette grammaire est recursive a gauche, donc ...
Un exemple complet: derecursivisation En derecursivisant, on arrive a:
|
On va maintenant calculer Nullable, First et Follow dans l'ordre
Calcul de Nullable
|
Calcul de First On sait que :
|
On calcule First:
|
Calcul de Follow
On sait que :
|
On calcule Follow:
|
La table de l'automate On peut finalement construire la table de l'automate: On sait que :
|
On construit alors:
|
Enfin, notre programme d'analyse en Ocaml
exception Parse_Error;; type token = Int | PLUS | TIMES | LPAR | RPAR | Eof;; let descente gettoken = let tok = ref (gettoken()) in let advance () = tok := gettoken() in let eat t = if (!tok = t) then advance() else raise Parse_Error in let check t = if (!tok = t) then () else raise Parse_Error in let rec ntS () = match !tok with Int -> (ntE(); check(Eof)) | LPAR -> (ntE(); check(Eof)) | _ -> raise Parse_Error and ntE () = match !tok with Int -> (ntT(); ntE'()) | LPAR -> (ntT(); ntE'()) | _ -> raise Parse_Error and ntE' () = match !tok with PLUS -> (eat(PLUS); ntT(); ntE'()) | RPAR -> () (* transition vide *) | Eof -> () (* transition vide *) | _ -> raise Parse_Error and ntT () = match !tok with Int -> (ntF();ntT'()) | LPAR -> (ntF();ntT'()) | _ -> raise Parse_Error and ntT' () = match !tok with TIMES -> (eat(TIMES); ntF(); ntT'()) | PLUS -> () (* transition vide *) | RPAR -> () (* transition vide *) | Eof -> () (* transition vide *) | _ -> raise Parse_Error and ntF () = match !tok with Int -> (eat(Int)) | LPAR -> (eat(LPAR);ntE();eat(RPAR)) | _ -> raise Parse_Error in ntS();;
Enfin, notre programme d'analyse en Ocaml, avec traitement d'erreurs
(* on assume que l'on nous passe un analyseur lexical gettoken *) exception Parse_Error;; type token = Int | PLUS | TIMES | LPAR | RPAR | Eof;; let descente gettoken = let position = ref 1 and tok = ref (gettoken()) in let advance () = tok := gettoken(); position := !position+1 in let eat t = if (!tok = t) then advance() else raise Parse_Error in let check t = if (!tok = t) then () else raise Parse_Error in let perror s = print_string ("Expecting "^s^" at position: "); print_int !position; print_newline(); raise Parse_Error in let rec ntS () = match !tok with Int -> (ntE(); check(Eof)) | LPAR -> (ntE(); check(Eof)) | _ -> perror "int,(" and ntE () = match !tok with Int -> (ntT(); ntE'()) | LPAR -> (ntT(); ntE'()) | _ -> perror "int,(" and ntE' () = match !tok with PLUS -> (eat(PLUS); ntT(); ntE'()) | RPAR -> () (* transition vide *) | Eof -> () (* transition vide *) | _ -> perror "+,),eof" and ntT () = match !tok with Int -> (ntF();ntT'()) | LPAR -> (ntF();ntT'()) | _ -> perror "int,(" and ntT' () = match !tok with TIMES -> (eat(TIMES); ntF(); ntT'()) | PLUS -> () (* transition vide *) | RPAR -> () (* transition vide *) | Eof -> () (* transition vide *) | _ -> perror "+,*,),eof" and ntF () = match !tok with Int -> (eat(Int)) | LPAR -> (eat(LPAR);ntE();eat(RPAR)) | _ -> perror "int,(" in ntS();;
This document was translated from LATEX by HEVEA.