Analyse syntaxique : JFlex et Cup

publicité
Miage FA FC 2010/2011
Analyse syntaxique
mai 2011
Analyse syntaxique : JFlex et Cup
1
Introduction
Cup est un générateur d'analyseur syntaxique pour les grammaires LALR(1). Il fonctionne main
dans la main avec JFlex, qui lui fournit à la demande le token suivant. Ecrit en Java, on peut y
ajouter le code qu'on veut exécuter lorsqu'une règle est appliquée (table des symboles et déclarations
des variables, vérications de types . . .).
2
2.1
La chaîne de conception
Le principe
On construit un analyseur lexical reconnaissant les terminaux de la grammaire qu'on fournira à
Cup.
On obtient une classe java Yylex contenant cet analyseur lexical.
On fournit à Cup la description d'une grammaire algébrique (états terminaux, non terminaux,
axiome et règles de production).
A partir de cette description, Cup contruit un analyseur syntaxique LALR(1), sous forme d'une
classe Java.
On écrit un programme principal qui instantie un analyseur syntaxique, et qui traiteun chier
vériant la grammaire.
2.2
Un exemple
L'exemple étudié ici est minimal, il sert uniquement d'une part à vérier que votre environnement
est bien conguré, d'autre part à comprendre la suite des opérations.
Tout d'abord, congurez l'environnement qui vous donnera accès à JFlex et Cup :
source /home/enseign/COMPILS5/envm5.sh
Allez chercher les trois chiers anLexInit.lex (la description de l'analyseur lexical déjà vu au
premier TP), parser.cup(la dénition de la grammaire), et MyParser.java (le programme de
test appelant l'analyseur syntaxique).
Construisez l'analyseur lexical :
jflex anLexInit.lex
ce qui vous fournit un chier Yylex.java. Reportez-vous au paragraphe 2.3.1 pour des explications
concernant le chier anLexinit.lex, et les diérences avec celui vu la fois dernière.
2
Miage FA FC 2010/2011 : Analyse syntaxique
On peut maintenant construire l'analyseur syntaxique à partir du chier parser.cup contenant
la description de la grammaire :
java -jar /usr/share/java/cup.jar parser.cup
Qui construit deux classes java : parser.java contenant l'analyseur syntaxique et sym.java
qui associe un entier à chaque token. Le paragraphe 2.3.2 détaille la structure d'un chier de dénition de grammaire.
On peut maintenant compiler le programme principal :
javac MyParser.java
Et l'exécuter à partir d'un chier test :
java MyParser test.txt
A ce point, vous devez avoir un analyseur syntaxique qui tourne. Sinon, revoyez la procédure avant
de continuer.
2.3
Dissection de l'exemple
La grammaire que l'on veut reconnaître est simpliste, ses règles de production sont
E −→ E + NOMBRE | NOMBRE
Où NOMBRE et '+' sont des terminaux. NOMBRE sera lui-même déni comme une suite de
chires, dans l'analyseur lexical (on aurait pu aussi le dénir dans la grammaire).
2.3.1 L'analyseur lexical
Le travail de l'analyseur lexical est de reconnaître sur l'entrée des nombres, des symboles '+', la n
de chier, les caractères inattendus (les erreurs), et la n de chier. Par rapport au TP précédent,
il ne fonctionne plus tout seul, on lui demande de fournir des tokens à l'analyseur syntaxique. De
plus, cet analyseur syntaxique attend des tokens d'une classe Java particulière : Symbol.
Référez-vous au TP précédent pour la syntaxe d'un chier de description lexicale.
anLexInit.lex commence par :
import java_cup.runtime.*;
qui permet d'avoir accès à la classe Symbol. Cette ligne sera copiée en entête de l'analyseur lexical
qui sera produit (Yylex.java).
Trois nouveautés dans la section suivante du chier :
La directive %cup qui assure la compatibilité avec la suite de la chaine de traitement.
la directive %type Symbol indiquant que les tokens retournés par l'analyseur lexical devront
être du type Symbol (voir le paragraphe 2.3.4 pour plus de détails sur cette classe)
La dénition d'une méthode retournant un objet de la classe Symbol pour chaque token reconnu. On aurait pu se passer de cette fonction et écrire return new Symbol(...) à chaque
action associée à la reconnaissance d'un token dans la troisième partie du chier.
La suite de cette deuxième partie du chier de description lexicale est similaire à celle que nous
avons vue lors du précédent TP.
la troisième partie du chier anLexInit.lex dénit principalement les actions à eectuer lorsqu'un token a été reconnu : ici, on envoie le token reconnu à l'analyseur syntaxique (plus quelques
println pour contrôler ou comprendre le déroulement du processus).
Analyse syntaxique : JFlex et Cup
3
2.3.2 Dénition de la grammaire et chier de spécication cup
Le chier parser.cup est sans doute ce que l'on peut faire de plus simple comme chier de description cup. Nous en verrons d'autres plus complets dans la suite des TPs. Comme les chiers de
description lexicale, un chier de description syntaxique se compose de plusieurs parties :
Des lignes de code java qui viendront avant le début de la dénition de la classe ; typiquement
les déclarations de paquetages et les imports.
Une partie
action code {: code java :}
contenant du code java qui sera ajoutée à une classe privée décrivant les actions à eectuer lors
de l'application d'une règle. Cette partie est vide dans notre exemple.
Une partie
parser code {: code java :}
contenant du code qui sera ajouté à la classe représentant l'analyseur syntaxique. Cette partie
est vide aussi.
Une partie
scan with {: code java :}
indiquant le nom de la fonction à appeler pour récupérer un token auprès de l'analyseur lexical.
Comme nous utilisons le nom par défaut (Yylex.yylex()), ce n'est pas la peine de la redénir,
et cette partie sera toujours vide.
La liste des terminaux (ici PLUS et NOMBRE). Un terminal peut avoir une valeur associée
(comme NOMBRE) ou pas (comme PLUS). On peut remarquer que ces non-terminaux vont
correspondre à des noms de constantes entières dans la classe sym. C'est aussi à ces constantes
que l'on fait référence dans l'analyseur lexical : la conception n'est pas tout à fait linéaire, car la
description cup inue sur la description lex.
La liste des non-terminaux, un seul dans notre cas, qui n'a pas de valeur associée (les nonterminaux peuvent aussi avoir des valeurs associées, on le verra plus tard).
La dénition de l'axiome :
start with Start;
.
La liste des règles de production.
2.3.3 Le programme principal
Réduit à sa plus simple expression, il instantie un analyseur syntaxique (de la classe parser) qui
utilise un analyseur lexical standard lisant son entrée dans le chier passé en paramètre.
2.3.4 la classe Symbol
Elle a deux constructeurs principaux :
public Symbol(int id,java.lang.Object o) où id est le numéro du token (déni dans la
classe sym), et o l'objet associé au token (comme NOMBRE).
public Symbol(int id) lorsque le token ne porte pas d'autre information que lui-même (comme
PLUS).
4
3
Miage FA FC 2010/2011 : Analyse syntaxique
Un peu plus loin avec cup
Deux choses importantes n'apparaissent pas dans l'exemple que nous avons étudié jusqu'ici :
On doit pouvoir associer une action à l'application d'une règle de la grammaire (comme on l'avait
fait pour l'analyseur lexical). Par exemple :
Si on reconnait un identicateur, vérier qu'il a été déclaré précédemment, ou bien qu'il est du
bon type.
Dans le cas de la grammaire des expressions, calculer la valeur de l'expression en cours d'analyse.
Pouvoir associer une valeur à un non-terminal (toujours dans la grammaire des expressions, par
exemple).
3.1
Associer une action à une règle
Comme pour l'analyse lexicale, on ajoute les actions après la description de la règle de production :
S::= XYZ {: code java qui sera exécuté lorsque cette règle sera appliquée :}
3.2
Associer une valeur à un non-terminal
Comme pour les terminaux, on peut déclarer qu'un non-terminal est porteur d'une valeur qui est
un objet Java :
nonterminal Integer Expression;
nonterminal String Message;
nonterminal ArrayList<String> LIST;
3.3
En combinant les deux. . .
. . . on peut par exemple calculer de proche en proche la valeur d'une expression.
Supposons que l'on ait la règle :
Expr ::= Expr PLUS Term;
La valeur associée à la partie gauche de cette règle peut être dénie comme étant la somme des
deux valeurs représentées en partie droite. Cela s'écrit de la façon suivante :
Expr::= Expr:v1 PLUS Term:v2 {: RESULT=v1+v2 :} ;
si on suppose que Expr et Term ont été dénis par :
nonterminal Integer Expr, Term
4
A vous
Dénir une grammaire pour les expressions standards, et testez le résultat sur diérents exemples
(respectant ou non la grammaire).
Complétez la grammaire précédente pour calculer et acher la valeur d'une expression.
Ajouter des identicateurs. Attention, une expression ne pourra être évaluée que si tous les
identicateurs apparaissant dans cette expression ont une valeur (sans doute un peu compliqué,
mais pas insurmontable).
Téléchargement