• Aucun résultat trouvé

Compilation TP Lexeur/Parseur C : Flex et Bison

N/A
N/A
Protected

Academic year: 2022

Partager "Compilation TP Lexeur/Parseur C : Flex et Bison"

Copied!
12
0
0

Texte intégral

(1)

Compilation TP Lexeur/Parseur

C : Flex et Bison

17 décembre 2021

Prérequis : ce TP suppose que vous avez initialisé un dépôt git avec un com- mit initial (avec uniquement le readme). Si ce n’est pas le cas, faites le.

Au début du TP, vous devez vous placer dans le dossier cloné, vous serez au- tomatiquement sur la branche master ,

1

qui sera la branche de rendu pour la seconde partie compilateur (si vous préférez qu’elle ai un autre nom, vous pouvez le changer).

Si vous n’êtes pas à l’aise du tout avec Git, vous pouvez regarder ces slides (niveau L2...). Si vous êtes perdu dans les branchements, utilisez l’interface graphique gitk ou un simulateur.

Attention : Une erreur courante sur ce TP consiste à remplacer brutalement un fichier par la correction ou par la version présente sur une autre branche git.

Il ne faut jamais faire ça, car l’algorithme de différenciation de git n’arrivera pas à suivre et les future merge vont tous échoué. Pour l’exercice, il faut modifier, à la main, chaque ligne qui a besoin d’être modifé. Si vous vous êtes trompé sur un branchement, rien de critique, cherchez en ligne ou demandez à l’enseignant comment revenir en arière avec Git.

2

Exercice 1 (Un parseur sans AST).

Attention, avec flex/bison, la séparation lexeur/parseur est différente du cours : on fait le maximum dans le parseur, et le lexeur n’est en apparence utilisé que pour les tokens non triviaux ; cela n’empêche pas que l’on passe toujours par le lexeur.

1. Créez trois fichiers :

— main.c qui est le programme exécutable,

— lexeur.l qui génère le lexeur,

— parseur.y qui génère le parseur.

2. Dans main.c , on se contente d’essayer de parser, et d’afficher si l’annalyse syntaxique à réussie ou échouée :

1. Celon le dépot git utilisé, votre branchemasters’appelleramain, dans ce cas remplacez simplement “master” par “main” dans toutes les commandes données.

2. l’avantage d’un gestionaire de version est que rien n’est jamais perdu, par contre l’outil est complexe et il faut apprendre à utiliser.

(2)

#include <stdio.h>

#include <stdlib.h>

int main(void) {

if (!yyparse()) { // call to the parsing (and lexing) function printf("\nParsing:: syntax OK\n"); // reached if parsing follows the grammar } exit(EXIT_SUCCESS);

}

La fonction yyparse() est générée par bison dans le code du parseur.

Cette fonction retourne 0 lorsque les analyses sémantiques et syntaxiques terminent avec succes,

3

1 lorsqu’il y a une erreur lexicale ou syntaxique, et 2 s’il y a une erreur innatendue (de mémoire principalement).

3. Dans parseur.y , on définit le parseur en utilisant des char comme TO- KEN :

/* file parseur.y

* compilation: bison -d parseur.y

* result: parseur.tab.c = C code for syntaxic analyser

* result: parseur.tab.h = def. of lexical units aka lexems

*/

%{ // the code between %{ and %} is copied at the start of the generated .c

#include <stdio.h>

int yylex(void); // -Wall : avoid implicit call

int yyerror(const char*); // on fonctions defined by the generator

%}

%token NUMBER // kinds of non-trivial tokens expected from the lexer

%start expression // main non-terminal

%% // denotes the begining of the grammar with bison-specific syntax expression: // an expression is

expression '+' term // either a sum of an expression and a term

| expression '-' term // or an expression minus a term

| term // or a term

;

term: // a term is

term '*' factor // either a product of a term and a factor

| factor // or a factor

;

3. De nos jours un programme qui renvoit0pour réussir est très étrange, mais n’oubliez pas que Bison est créé sur la base de Yacc, lui même écris début des années 70 pour le langage B...

(3)

factor: // a factor is

| '(' expression ')' // either an expression surounded by parentheses

| '-' factor // or the negation of a factor

| NUMBER // or a token NUMBER

;

%% // denotes the end of the grammar

// everything after %% is copied at the end of the generated .c

int yyerror(const char *msg){ // called by the parser if the parsing fails printf("Parsing:: syntax error\n");

return 1; // to distinguish with the 0 retured by the success }

Dans l’ordre :

— on définit le prototype du lexeur et de la fonction d’erreur de bison

4

— on y décrit un unique token non trivial (càd qui n’est pas un simple char ) appelé NUMBER ,

— on y décrit une grammaire avec expression , term et factor comme non terminaux et avec '+' , '-' , '*' , '/' , '(' , ')' , '-' ainsi que le token NUMBER comme terminaux.

— on inclue des bibliothèques pour pouvoir écrire le programme renvoyé en cas d’erreur,

— on y décrit la fonction yyerror qui est appelée en cas d’erreur 4. Dans lexeur.l , on définit l’unique token non trivial :

/* file lexeur.l

* compilation: flex lexeur.l

* result: lex.yy.c = lexical analyser in C

*/

%{ #include <stdio.h> // printf

#include "parseur.tab.h" // token constants defined in parseur.y via #define

%}

%%

0|[1-9][0-9]* { printf("lex::NUMBER %s\n",yytext); return NUMBER; } [ \t\n] { ; } // ignore space and tab

[ \t\n] { return 0; } // exit sucessfully

. { printf("lex::char %s\n",yytext); return yytext[0]; } // other one-char = token, ex. '+'

%% int yywrap(void){ return 1; } // function called at the end of the file

4. Il s’agit d’éviter certains warning à la compilation : cf.

https://stackoverflow.com/questions/20106574/simple-yacc-grammars-give-an-error

(4)

Dans l’ordre :

— on inclut parseur.tab.h qui est généré à partir de parseur.y et qui définit le token NUMBER ,

— 0|[1-9][0-9]* est l’expression régulière capturant les entiers,

— la ligne “ printf("lex::NUMBER %s\n",yytext); return NUMBER; ” est l’action associée à l’expression régulière : lorsque le lexeur a re- connu l’expression en question il va donc afficher “ lex::NUMBER%s "

sur le terminal où %s est remplacé par le lexème reconnu, puis il va envoyer le token NUMBER au parseur avant de continuer à chercher le prochain lexème,

— la ligne [ \t] {;} permet d’ignorer les séparateurs (ici l’espace et la tabulation),

— la ligne \n {return 1;} permet d’arrêter le lexeur (et donc le par- seur) au premier retour à la ligne,

— la ligne . {return yytext[0];} dit que si on lit autre chose, on renvoit au parseur un token trivial avec ce caractère,

— la fonction yywrap est appellée à la fin du fichier, elle doit toujours renvoyer 1.

5. Compilez tout ça à l’aide des trois commandes suivantes dans le terminal :

$ bison -d parseur.y

$ flex lexeur.l

$ gcc -o main main.c parseur.tab.c lex.yy.c

Cela génère deux fichiers intermédiaires : votre parseur parseur.tab.c , votre lexeur lex.yy.c , puis votre exécutable main

6. Vous pouvez lancer main dans un terminal entrez une expression arith- métique, si elle est correct vous aurez un message l’indiquant sinon vous aurez un message d’erreur.

7. Vous pouvez ajouter ces trois fichier au suivit de git, commit-er et push- er :

$ git add lexeur.l parseur.y main.c

$ git commit -m "mon premier parseur"

$ git push

master head premier parseur

8. On va introduire une feature, pour ne pas polluer la branche master, on crée une branche séparée (ici appelée parser_work ) et se mettre dessus :

$ git checkout -b parser_work

master parser_work head premier parseur

(5)

9. On voudrait ne reconnaitre que des expressions finissant par un “ ;”. Pour ça, on va modifier parseur.y :

— ajouter un non-terminal commande qui reconnaisse une expression sui- vie d’un “ ;”

— changez le non-terminal principal (celui après %start ) pour y mettre commande

Ajouter une expression régulière pour cela, et créez un nouveau token

\PT_VIRG sans oublier de modifier la grammaire du parseur.

10. Faites un commit et un push de vos changements :

$ git add lexeur.l parseur.y

$ git commit -m point_virg

$ git push

Remarquez que l’on refait un git add , en fait celui-ci sert aussi à spécifier les fichiers que l’on a mis à jours.

master parser_work head premier parseur point_virg

11. On veut maintenant voir si on peut rajouter une feature du fragment 0.1, or même si on n’a pas encore tout ce qu’il faut pour le fragment 0.0, on peut tout de même s’avancer en créant une nouvelle branche temporaire :

$ git checkout -b TP

master parser_work TP head premier parseur point_virg

12. Rajoutez une écriture décimal (virgule fixe) pour nos nombres en modi- fiant l’expression régulière corespondante (dans le fichier lexeur.l ).

Faites un nouveau commit. La situation de votre dépôt devrait alors être la suivante :

master parser_work TP head premier parseur point_virg decimals

Exercice 2 (Simplification et ouverture de fichiers).

1. Revenez sur la branche master :

$ git checkout master

master parser_work TP head

premier parseur point_virg decimals

(6)

2. Nous allons simplifier la grammaire en spécifiant des priorités et associa- tivité.

Allez dans parseur.y et modifiez ainsi le fichier :

%token NUMBER

%left '+' '-'

%left '*'

%nonassoc MOINSU

%%

result: expression expression:

expression '+' expression

| expression '-' expression

| expression '*' expression

| '(' expression ')'

| '-' expression %prec MOINSU

| NUMBER

;

3. Cette version fait exactement la même chose que la précédente, mais en plus concis et intuitif grâce aux lois de priorité et d’associativité :

— Les règles d’associativité sont indiqués par %left ou %right .

— Les règles de priorité sont implicites : '*' est prioritaire sur '+' et '-' car la ligne “ %left '+' '-' ” est placée avant la ligne “ %left '*' ”.

— Lorsque l’associativité de fait aucun sens (par exemple pour un opéra- teur unaire) mais que l’on veut avoir une priorité, on utilise %nonassoc et on place les opérateur au bon niveau.

— Lorsqu’un token est utilisé dans plusieurs règles avec des priorités/associativités différentes (comme '-' ), on utilise une balise pour indiquer la priorité

d’une des règles, ici la seconde règle du moins est balisée UMOINS qui a une autre priorité que '-' .

4. Faites un commit :

master

parser_work TP head

premier parseur simplification

point_virg decimals

5. On veut rajouter ces changements dans la branche de travail du parseur, pour ça on revient sur cette dernière, et on la merge avec master :

$ git checkout parser_work

(7)

master

parser_work TP head

premier parseur simplification

point_virg decimals

$ git merge master

master parser_work

TP head

premier parseur

simplification point_virg

merge decimals

6. On voudrait maintenant finir le fragment 0.0 du parseur, mais pour ça, il faut permettre à l’utilisateur de donner un fichier en argument :

— dans lexeur.l : supprimez la ligne sur \n et ajoutez le retour à la ligne parmis les séparateurs,

— dans main.c : créez un pointeur de fichier vers le chemin d’accès dpnné en premier argument de votre programme, puis donnez ce pointeur à la fonction yyparse .

7. Faites un commit, puis créez la branche de rendue parseur et tagg-ez le commit et revenez sur la branche de travail :

$ git add main.ml

$ git commit -m input

$ git checkout -b parser

$ git tag -a p0.0 -m ``premiere version faites en TP par <mon nom>''

$ git push

$ git checkout parser_work

p0.0

master

parser_work parser

TP head

premier parseur

simplification point_virg

merge input

decimals

8. On va maintenant pouvoir récupérer les décimaux que l’on avaient ajouter tout à l’heure. Pour ça il suffit de merger avec TP , après quoi on peut supprimer TP qui n’est plus utile :

$ git merge TP

(8)

$ git push

$ git branch -d TP

$ git push --delete TP

9. Rajoutez l’opérateur de pourcentage (_%_) (attention à sa priorité !), faites un commit, revenez sur la branche parser , mergez et placez le tag p0.1 :

p0.0 p0.1

master

parser_work parser head

premier parseur

simplification point_virg

merge

input merge mod

merge

decimals

Exercice 3 (Aparte : interpréteur). Il n’est pas demandé, dans le projet, de faire un interpreteur car ce serait plus difficile d’implémenter variables et fonctions que de faire un compilateur vers notre assembleur abstrait. Néamoins, pour les tout premiers fragments, il se trouve que c’est assez simples, et on va le faire pour se familiariser avec l’outil de parsing.

1. Revenez sur master, créez une nouvelle branche appelée interpreter . 2. Commencez par modifier le lexeur afin qu’il transmette la valeur des

entiers sont lus (pour l’instant il ne fait que dire qu’il a vu un entier...).

Pour ça, on modifie la ligne qui permet de reconnaître NUMBER afin de passer l’entier reconnu :

0|[1-9][0-9]* { printf("lex::NUMBER %s\n",yytext);

yylval=atoi(yytext); return NUMBER; } la variable yylval est celle qui contiendra le comptenu de token à sa création, la variable yytext est celle qui contient le lexeme lu (c’est donc une string).

3. Ensuite il faut exprimer les contenus créés à chaque règle : expression:

expression '+' expression { $$ = $1+$3; }

| expression '-' expression { $$ = $1-$3; }

| expression '*' expression { $$ = $1*$3; }

| '(' expression ')' { $$ = $2; }

| '-' expression %prec MOINSU { $$ = -$2; }

| NUMBER { $$ = $1; } // default semantic value

;

(9)

Dans les actions (entre les acolades), $1,$2,$3 désignent le contenu du premier, second et troisième token utilisé dans la rêgle. Les actions, ici, sont de type entier, c’est l’entier que l’on va mettre dans le token créé.

4. Vous pouvez tester et faire un commit (appelé interp dans la suite) si tout va bien.

5. Faites le merge avec p0.1 . Le programme résultant ne sera pas fonctionnel car on a typé nos sorties avec des entiers au lieux de flottants et car le modulo n’est pas écrit.

6. Par défaut les tokens de bison ont le type int , mais ici, on veut utiliser d’autres types associés aux tokens il faut, pour ça, ajouter un type union dans parseur.y avec :

%union { double dval; int ival; } ; et modifier le lexeur en conséquence : yylval.dval=...

7. Pour indiquer au parseur que le terminal expression et le non terminal NUMBER vont créer des tokens contenant des doubles il faut utiliser

%token <dval> NUMBER

%type <dval> expression

8. Corrigez tout ça, faites un commit, et tagg-ez le. Votre dépôt doit res- sembler à ça :

i0.0 i0.1

p0.0 p0.1

master parser_work

parser

interpreter

head

premier parseur

simplification interp

merge merge corrections

point_virg merge

input merge mod

merge

decimals

Dans les exercices suivants, on oubliera la branche “interpreter”. En effet, l’inter- préteur n’est pas demandé pour le projet contrairement au compilateur (l’inter- préteur devient difficile une fois que l’on parle de variable, très difficile lorsque l’on introduit les fonctions, et encore plus avec les exceptions).

Exercice 4 (Créer un AST en sortie). Avant de pouvoir faire un interpreteur, on

veut pouvoir manipuler notre AST. Ce n’est pas strictement nécessaire, surtout

au début, mais c’est très pratique pour s’y retrouver et séparer les problèmes.

(10)

1. Revenez sur la branche master , et téléchargez les fichiers AST.h et AST.c disponibles ici.

2. Vous y trouverez la structure de l’AST dans AST.h , et les fonctions pour la manipuler implémentées dans AST.c .

3. Nous alons utiliser ce type d’AST comme type de retour des expressions.

Pour expliquez ça au parseur, dans parseur.y , nous devons rajouter :

— un #include "AST.h" avecles autres,

— après les %{***%} , on ajoute la ligne

%parse-param {struct _tree* *pT}

qui permet à la fonction yyparse dans main.c de prendre un pointeur de retour vers notre ast vide en paramètre.

— juste après, comme à l’exercice précédent, on rajoute un type union :

5

%union {

struct _tree* exp;

int num;

} ; %type <exp> expression

%token <num> NOMBRE

— il faut rajouter un non-terminal result qui serale terminal principal et ne contient qu’une expression (celà permet de metre le bon AST dans le pointeur de retour).

— il faut maintenant écrire les actions associées à la grammaire permet- tant de construire un AST. :

result: expression { *pT = $1; } expression:

expression '+' expression { $$ = newBinaryAST('+',$1,$3); }

| expression '-' expression { $$ = newBinaryAST('-',$1,$3); }

| expression '*' expression { $$ = newBinaryAST('*',$1,$3); }

| '(' expression ')' { $$ = $2; }

| '-' expression %prec MOINSU { $$ = newUnaryAST('-',$2); }

| NUMBER { $$ = newLeafAST($1); }

;

par exemple { $$ = newBinaryAST('+',$1,$3); } indique l’action effectuée lorsque l’on lit le pattern correspondant : on appelle la fonc- tion newBinaryAST qui renvoit un nouvel arbre syntaxique créé à partir de $1 et $3 qui sont eux-même les arbres renvoyés par les ex- pressions à gauche et à droite de la somme.

6

4. Comme dans l’exercice précédent, dans lexeur.l , il y a un unique chan- gement à faire : yylval devient yylval.num pour informer le parseur du champs ( num de type int ) associé au TOKEN NUMBER .

5. Cette complexité vient de l’utilisation sous-jacente duunionde C qui entre autre fait chevaucher les différents champs en mémoire

6. On passe de $1 à $3 car $2 est le retour (du lexeur) sur ’+’, que l’on n’utilise pas.

(11)

5. Vous pouvez tester et faire un commit (appelé intro_ast dans la suite) si tout va bien.

6. Faites une nouvelle branche ast et mergez avec p0.0 .

7. Ceci n’est pas encore notre compilateur, il faut encore générer du code, mais avant ça, nous allons nous entraîner à maîtriser l’AST. Pour pouvoir revenir sur la génération de code, veuillez créer une nouvelle branche code_gen puis revenir sur ast et mergez avec p0.1 .

8. Rajoutez l’AST du reste et des flottants.

9. Sur une branche temporaire séparée (ceci est un exercice, et n’est pas utile pour le projet), écrivez un programme qui évalue l’expression entrée en parcourant l’arbre syntaxique généré.

p0.0 p0.1

master

parser_work parser

ast code_gen

head tmp

premier parseur

simplification intro_ast

merge merge compl_ast

inerp_ast

point_virg merge

input merge mod

merge

decimals

Exercice 5 (Génération de code).

1. Revenez sur code_gen ,

2. Dans AST.c , créez une nouvelle fonction d’affichage code qui va parcourir l’arbre et va faire un affichage post-fixe de sa structure en affichant :

7

— la ligne “ CsteNb n” après avoir quitté un noeud Nombre (n),

— la ligne “ AddiNb ” après avoir quitte un noeud Plus ,

— la ligne “ MultNb ” après avoir quitte un noeud Mult ,

— la ligne “ SubiNb ” après avoir quitte un noeud Moins ,

— la ligne “ NegaNb ” après avoir quitte un noeud Neg ,

3. Un fichier .js est sensé être donné en argument de notre compilateur, faites en sorte que soit généré automatiquement un nouveau fichier de même nom mais avec l’extension .jsm

4. Testez votre code à l’aide de la miniJSMachine.

7. Passez à la ligne après chaque affichage pour plus de lisibilité.

(12)

5. Commitez, mergez master avec cette branche, placez votre tag c0.0 , et revenez sur la branche code_gen .

6. Mergez avec la branche interpreter , et corrigez les conflits si besoin.

7. Complétez la génération de code à l’aide de la commande assembleur ModuNb pour le modulo.

8. Testez, commitez, mergez master avec cette branche, puis placez votre tag c0.1 .

c0.0 c0.1

p0.0 p0.1

master

parser_work parser

ast

code_gen

premier parseur

simplification intro_ast

merge

merge

merge merge compl_ast

code_0.0

code_0.1 merge

inerp_ast

point_virg merge

input merge mod

merge

decimals

Vous pouvez maintenant passer au TP2. À partir de maintenant les TPs :

— seront langage agnostiques,

— ne donneront plus de détails d’implémentation, et

— ne préciseront plus la structure interne de votre dépôt git.

Seules les brancher master et parseur sont demandées, pour les autres, vous

pouvez fonctionner comme bon vous semble. Mais il est fortement recommandé

de fonctionner ainsi par couches successives, c’est plus pratique pour débuguer,

mais aussi pour travailler en parallèle avec des vitesses différentes. N’hésitez pas

non plus à poser d’autres tags afin de pouvoir merger un point particulier tout

en continuant sur une branche.

Références

Documents relatifs

On peut utiliser checkout avec en référence le nom d’une branche (on remonte alors à la tête de la branche), un tag ou une référence sha1 pour se déplacer dans l’historique

Eh bien l’être parlant veut dire qu’il y a des êtres dont c’est la nature de parler, la propriété, la supériorité ( ?), et l’être parlé – notion passive aux yeux de

– test : lance les algorithmes sur une expression rationnelle et un ensemble de mots dans un fichier, – documentation : génére une documentation en HTML des modules qui vous

Les tests de substitution n’étant pas valables ici, quels indices pourraient nous aiguiller sur la (non)-coréférence établie entre le déclencheur d’antécédent

Notre travail, c'est d'attaquer ce qui peut céder, avant l'incrustation quasi-défi- nitive, alors que les êtres sont jeunes et près, par l e temps, de

115 L’analyse de l’activité de travail réalisée dans le cas du modèle du flex  office dans l’immobilier d’entreprise de cette firme du

• Parseur = fonction qui prends une liste de caractères en entrée et qui retourne un Booléen ( #t = la liste de caractères est une phrase qui fait partie du langage, sinon #f ).. •

Marie, quant à elle, ne voit plus l’intérêt du sujet qu’elle a pourtant choisi avec enthousiasme et pour lequel elle est allée passer des mois au Honduras… «