• Aucun résultat trouvé

Compilation TP Lexeur/Parseur OCaml : Ocamllex et OCamlyacc

N/A
N/A
Protected

Academic year: 2022

Partager "Compilation TP Lexeur/Parseur OCaml : Ocamllex et OCamlyacc"

Copied!
12
0
0

Texte intégral

(1)

Compilation TP Lexeur/Parseur

OCaml : Ocamllex et OCamlyacc

17 décembre 2021

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

Au début du TP, vous devez vous placer dans le dossier cloné, vous serez automatiquement 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 pouvezregarder ces slides (niveau L2...). Si vous êtes perdu dans les branchements, utilisez l’interface graphiquegitkouun 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).

1. Créez trois fichiers :

— main.mlqui est le programme exécutable,

— lexeur.mllqui génère le lexeur,

— parseur.mlyqui génère le parseur.

2. Dans main.ml, on se contente d’essayer de parseur en boucle jusqu’à avoir une erreur :

(*fichier main.ml *)

let _ = (*main en OCaml*)

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)

trylet lexbuf = Lexing.from_channel stdin in (*lexeur lancé sur stdin*)

while true do (*on ne s'arrête pas*)

Parseur.main Lexeur.token lexbuf (*parseur une ligne*) withdone

| Lexeur.Eof -> exit 0 (*impossible*)

| Lexeur.TokenInconu (*erreur de lexing*)

| Parsing.Parse_error -> (*erreur de parsing*) Printf.printf ("Ceci n'est pas une expression arithmetique\n") plus précisément :

— la ligne “let lexbuf = Lexing.from_channel stdin in” crée un scanner ,3

— la ligne “Parseur.main Lexeur.token lexbuf” appel le parseur avec le lexeur et le scanner comme arguments,4

— la première exception Lexeur.Eofn’est pas utile ici : il n’y aura pas de symbole de fin de fichier en lisant le stdin, mais je la met pour ne pas que vous l’oubliez plus tard...

— les deux autres exceptions reportent les blocages du lexeur et du par- seur. Notez qu’il s’agit d’un catch multiple : les deux exceptions vont afficher le même printf.5

3. Dans parseur.mly, on spécifie le parseur :

%token NOMBRE PLUS MOINS FOIS GPAREN DPAREN EOL

%type <unit> main expression terme facteur

%start main

%%main:

expression EOL {}

expression:;

expression PLUS terme {}

| expression MOINS terme {}

| terme {}

terme:;

terme FOIS facteur {}

| facteur {}

;

3. que l’on appelle traditionnellement lexbuf, car il s’agit du fichier à lexeur mis dans un buffeur.

4. Pour ceux qui ont suivi le cours de M.Leroux l’an dernier : normalement on devrait donner l’entrée au lexeur qui calcule la liste de token qui est donnée au parseur, mais ici c’est le parseur qui va paresseusement lancer le lexeur pour récupérer les tokens un à un.

5. Le report d’erreur de compilation n’étant pas considéré dans ce projet, vous pouvez renvoyer ce que vous souhaitez. N’hésitez pas à faire des affichages séparés dans votre version.

(3)

facteur:

GPAREN expression DPAREN {}

| MOINS facteur {}

| NOMBRE {}

;

Dans l’ordre :

— on y déclare nos noms de tokens,

— on déclare le type (iciunit) de nos non-terminaux,

— on déclare le non-terminal principal (icimain),

— on y décrit une grammaire, si elle est si compliquée, c’est pour forcer les priorités.

— les accolades {} indiquent qu’il n’y a aucune action résultante de la lecture de ces patterns.

4. Dans lexeur.mll, on définit les tokens : (*fichier lexeur.mll *)

{ open Parseur exception Eof

exception TokenInconu }rule token = parse

[' ' '\t'] { token lexbuf }

| ['\n'] { EOL }

| ['0'-'9']+ { NOMBRE }

| '+' { PLUS }

| '-' { MOINS }

| '*' { FOIS }

| '(' { GPAREN }

| ')' { DPAREN }

| eof { raise Eof }

| _ { raise TokenInconu }

Dans l’ordre :

— on inclueParseurqui est sera généré à partir deparseur.mlyet qui définit les tokens (avec leur types),

— la ligne [' ' '\t'] {token lexbuf} reconnaît les espaces et tabu- lations mais ne crée pas de token car ce sont desséparateurs; le “token lexbuf” est simplement une façon de dire au lexeur de continuer.

— la ligne ['\n'] {EOL} crée le token EOL en cas de changement de ligne,

— le ['0'-'9']+est l’expression régulière capturant les entiers,

— juste après le “{NOMBRE}” est l’action associée, ici on retourne simple- ment le tokenNOMBRE,

— les 6 lignes suivantes récupère les symboles d’opération et y associe les tokens correspondants,

(4)

— les deux dernières lignes lancent des exceptions (récupérées dansmain.ml) si on lit la fin du fichier ou un symbole inconnu.

5. Compilez tout ça à l’aide des commandes suivantes dans le terminal (ou faites un makefile) :

$ ocamllex lexeur.mll

$ ocamlyacc parseur.mly

$ ocamlc -c parseur.mli lexeur.ml parseur.ml main.ml

$ ocamlc -o main lexeur.cmo parseur.cmo main.cmo

Cela génère plusieurs de fichiers intermédiaires, parmi lesquels votre lexeur lexeur.ml, votre parseurparseur.ml, et votre éxécutablemain.

6. Vous pouvez lancermaindans un terminal et taper une expression avant d’aller à la ligne. Si elle est correcte rien ne se passe mais si elle est fausse vous aurez un message d’erreur.

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

$ git add lexeur.mll parseur.mly main.ml

$ git commit -m "mon premier parseur"

$ git push

master head premier parseur

8. On va introduire unefeature, pour ne pas polluer la branche master, on crée une branche séparée (ici appeléeparser_work) et se rentre dedans :

$ git checkout -b parser_work

9. Jusque là, on lançait le scan lorsque l’on arrivait sur une nouvelle ligne (lorsque l’on voyait le token EOL). On voudrait pouvoir écrire plusieurs lignes et finir par un “ ;”. Pour ça ajoutez le retour à la ligne'\n'parmi les séparateurs, enlevez le tokenEOLet créez un nouveau token\PT_VIRG. 10. Faites un commit et un push de vos changements :

$ git add lexeur.mll parseur.mly

$ git commit -m "point_virg"

$ git push

Remarquez que l’on refait ungit 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

(5)

11. On veut maintenant voir si on peut rajouter une feature du fragment0.1, or on n’a pas encore tout ce qu’il faut pour le fragment0.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.mll).

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

2. Allez dansparseur.mlyet modifiez ainsi le fichier :

%token NOMBRE PLUS MOINS FOIS GPAREN DPAREN EOL

%left PLUS MOINS

%left FOIS

%nonassoc UMOINS

%type <unit> main expression

%start main

%%main:

expression EOL {}

expression:;

expression PLUS expression {}

| expression MOINS expression {}

| expression FOIS expression {}

(6)

| GPAREN expression DPAREN {}

| MOINS expression %prec UMOINS {}

| NOMBRE {}

;

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 %leftou%right.

— Les règles de priorité sont implicites :FOISest prioritaire surPLUSet MOINScar%left PLUS MOINS est définit avant%left FOIS.

— Lorsque l’associativité de fait aucun sens (par exemple pour un opéra- teur unaire) mais qui 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 (commeMOINS), on utilise une balise pour indiquer la prio-

rité d’une des règles, ici la seconde règle du moins est baliséeUMOINS qui a une autre priorité queMOINS.

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

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

(7)

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. Pour ne pas casser ce que l’on a déjà, on va lancer la version terminal lorsque l’utilisateur ne donne pas d’argument, sinon on ouvre le premier argument comme un fichier. Implémentez cette feature.

7. Faites un commit, puis créez la branche de rendueparseuret 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'' On est alors dans la situation suivante :

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 avait ajoutés tout à l’heure. Pour ça il suffit de merger avec TP, après quoi on peut supprimerTPqui n’est plus utile :

$ git merge TP

$ git push

$ git branch -d TP

$ git push --delete TP

9. Rajoutez l’opérateur de modulo (_%_) (attention à sa priorité !), faites un commit, revenez sur la brancheparseret placez letag p0.1:

p0.0 p0.1

master

parser_work parser head

premier parseur

simplification point_virg

merge

input merge mod

merge

decimals

(8)

Exercice 3 (Aparte : interpréteur). Il n’est pas demandé, dans le projet, de faire un interpreteur car ce serait plus difficile qu’un compilateur vers notre assembleur abstrait. Mais pour les tout premiers fragments, c’est plus simples, et on va le faire pour se familiariser avec l’outil de parsing.

1. Revenez sur master, créez une nouvelle branche appeléeinterpreter. 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îtreNOMBREen :

| ['0'-'9']+ as lexem { NOMBRE(int_of_string lexem) } 3. Puis on modifie la première ligne du parseur afin de lui signaler que les

tokensNOMBREont maintenant un contenu :

%token <int> NOMBRE

%token PLUS MOINS FOIS GPAREN DPAREN EOL

4. Après ça, il faut dire au parseur que les non-teminaux aussi vont créer des tokens contenant des entiers :

%type <int> main expression

(attention cette ligne remplace une ligne existante, je suis sure que vous allez trouver laquelle).

5. Ensuite il faut exprimer les contenus créés à chaque rêgle : main:

expression EOL { $1 }

expression:;

expression PLUS expression { $1+$3 }

| expression MOINS expression { $1-$3 }

| expression FOIS expression { $1*$3 }

| GPAREN expression DPAREN { $2 }

| MOINS expression %prec UMOINS { -$2 }

| NOMBRE { $1 }

;

Dans les actions (entre les acolades), $1,$2,$3dé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éé.

6. Enfin dansmain.ml, on va afficher l’entier trouvé :

Parseur.main Lexeur.token lexbuf (*parseur une ligne*)

|> Printf.printf "%i\n%!";

La fonction Parseur.main, avec le lexeur et l’entrée standard en argu- ments, retourne l’entier calculé, on affiche celui-ci avant de passer à la ligne et de flush (d’où le%!).

(9)

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

8. Faites le merge avec p0.0afin de pouvoir prendre des fichiers en entrée.

Corrigez les conflits si besoin et tagg-ez la version i0.0.

9. Faites le merge avecp0.1. Le programme résultant ne sera pas fonction- nel car on a typé nos sorties avec des entiers au lieux de flottants et car le modulo n’est pas écrit. Corrigez tout ça, faites un commit et tagg-ez le. Votre dépôt doit ressembler à ç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).

Exercice4 (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.

1. Revenez sur Master, et téléchargez le module d’AST disponible ici.6 2. Vous y trouverez la structure de l’AST et une fonction d’affichage (elle

est étrange car j’utilise une bibliothèque de formatage qui fait de l’inden- tation automatique).

3. Comme dans l’exercice précédent, dans lexeur.mll, il y a un unique changement à faire : la ligne

['0'-'9']+ as lexem { NOMBRE(int_of_string lexem) }, qui indique que le token NOMBREest accompagné de l’entier int_of_string lexemoùlexemest le contenu du buffeur.7

6. Si vous êtes avancé en OCaml, vous remarquerez que l’on n’a pas fait un vrais module, c’est juste pour rendre le code plus accessible, n’hésitez pas à faire un commit (d’abord dans une branche de travail !) qui en fait un module propre.

7. Dans le jargon de compilation, le contenu du buffeur sur la lecture d’un token est appelé lexème

(10)

4. Comme précédemment, on modifie la première ligne du parseur afin de lui signaler que les tokensNOMBREont maintenant un contenu entier.

5. Après ça, il faut dire au parseur que les non-teminaux vont créer des tokens contenant un AST :

%type <AST.expression_a> main expression

6. Ensuite il faut exprimer les contenus créés à chaque rêgle : main:

expression EOL { $1 }

expression:;

expression PLUS expression { Plus ($1,$3) }

| expression MOINS expression { Moins($1,$3) }

| expression FOIS expression { Mult ($1,$3) }

| GPAREN expression DPAREN { $2 }

| MOINS expression %prec UMOINS { Neg $2 }

| NOMBRE { Num $1 }

;

Cette fois on appel les générateurs du type AST pour créer l’arbre à la volée.

7. Avant de fermer parseur.ml, il faut permettre à OCaml de trouver le module d’AST. Pour ça on ajoute les lignes suivantes tout au début du fichier :

%{open AST

%}

tout ce qui est entre acolades %{...%} sera copié au début du parseur généréparseur.ml.

8. Enfin dansmain.ml, on va afficher l’arbre trouvé :

Parseur.main Lexeur.token lexbuf (*parseur une ligne*)

|> Format.printf "%a\n%!" AST.print_AST ;

Le%apermet l’afichage d’un élément de type quelconque pourvu que l’on lui donne une fonction d’affichage inteligente avec (iciAST.print_AST).

9. Vous pouvez tester et faire un commit (appelé intro_astdans la suite) si tout va bien.

10. Faites une nouvelle brancheastet mergez avecp0.0.

11. 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_genpuis revenir surastet mergez avecp0.1.

12. Rajoutez l’AST du modulo et des flottants.

(11)

13. 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.ml, créez une nouvelle fonction d’affichage8 code qui va par- courir l’arbre et va faire un affichage post-fixe de sa structure en affichant :

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

— la ligne “AddiNb” après avoir quitté un noeudPlus,

— la ligne “MultNb” après avoir quitté un noeudMult,

— la ligne “SubiNb” après avoir quitté un noeudMoins,

— la ligne “NegaNb” après avoir quitté un noeudNeg,

3. Dans le cas où fichier est donné en argument de notre compilateur, on voudrait 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.

5. Commitez, mergez masteravec cette branche, placez votre tagp0.0, et revenez sur la branchecode_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 tagp0.1.

8. Vous pouvez vous aussi utiliser la bibliothèqueFormatsi vous souhaitez la découvrir, mais elle n’est pas nécessaire ici, la bibliothèquePrintfest suffisante.

(12)

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 branchermasteretparseursont 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

This pattern requires mutually-recursive dependence among the subject and observer classes; the observer must be aware of the type of the subjects in order to notify them and

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

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

Sélectionnez 2 ou 3 lexems simples et/ou triviaux de la liste en annexe (au choix du chargé de TD) et construisez le lexeur associé.. Exercice 2

An d'utiliser plus d'un type de données pour les valeurs sémantiques dans un parser, Ocamlyacc impose de choisir un type de données pour chaque symbole (terminal ou non- terminal)

changement de type : bool´ een 7→ chaˆıne de caract` eres string of bool : bool -&gt; string chaˆıne de caract` eres 7→ bool´ een bool of string : string -&gt; bool

Using this kind of program, the application of the f function (used in the Sarek kernel) to each elements of the input vector into the output vector will take place in parallel using

On colore le point c avec un niveau de gris variant avec le rapport n/N.. Vous pouvez aussi tenter