Mini-projet Numéro 1 : Évaluation d`expressions arithméthiques et

Transcription

Mini-projet Numéro 1 : Évaluation d`expressions arithméthiques et
Mini-projet Numéro 1 :
Évaluation d’expressions arithméthiques et
booléennes
Version numéro 2
Alice BERNARD : bernar_l
Julien GRALL : grall_j
4 novembre 2009
Table des matières
1 Instructions
2
2 Code fourni
5
3 Pretty printing
6
4 Premiers pas de l’évaluation
7
5 Évaluation
9
6 Validation et typage
10
7 Rendu
11
7.1 Utilisation du parser . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
7.2 Programme principal . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
8 Conseil
8.1 Les structures de données . . . . . . . . . . . . . . . . . . . . . . . .
8.2 Le typage des opérateurs . . . . . . . . . . . . . . . . . . . . . . . . .
8.3 Le parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
13
13
14
9 Exemples
15
1
Chapitre 1
Instructions
Veuillez lire le sujet en entier avant de commencer.
Toutes les questions dont la réponse se trouve dans le sujet se verront rejetées.
Consignes à respecter Ce Mini-projet est à réaliser SEUL.
Le rendu devra être sous la forme d’une archive tar compressée avec bzip2. La
règle dist de votre Makefile devra le faire pour vous. Le nom de l’archive sera :
login_x-mp1.tar.bz2 (où login_x est votre login).
Arboresence de votre rendu login_x-mp1/
login_x-mp1/AUTHORS
login_x-mp1/Makefile
login_x-mp1/parser.[ml-mli-mly]
login_x-mp1/lexer.[mll-ml]
login_x-mp1/ast.mli
login_x-mp1/*.ml
AUTHORS Votre fichier AUTHORS suivra la Coding style Epita :
une étoile, un espace, votre login, un retour à la ligne.
42sh$ cat -e AUTHORS
* login_x$
42sh$
Où 42sh$ représente votre prompt.
Modalités de rendu Le rendu sera ouvert à partir du samedi 14 novembre à
23h59.
Il fermera le dimanche 15 novembre à 23h42.
Vous avez le droit à un seul rendu, soyez sûr de votre projet lorsque vous le
rendez.
Aucun autre rendu sera accepté.
L’url sera mise à jour ultérieument et disponible sur le wiki.
Problèmes sur le sujet Si vous rencontrez le moindre problème dans le sujet,
vous pouvez poser vos questions aux responsables du sujet :
– Alice Bernard ([email protected])
– Julien Grall ([email protected])
2
Mais aussi à d’autres AOC qui sont dans la liste suivante :
– Mathilde Maury ([email protected])
– Gabriel Laskar ([email protected])
– Christophe Bretonnière ([email protected])
– Tangui Le Pense ([email protected])
Toutes les questions pertinentes seront recensées sur le google group :
http://groups.google.com/group/mp1-evalexpr
Des modifications du sujet peuvent avoir lieu.
Votre référence sera le sujet qui se trouve sur le wiki-prog :
http://wiki-prog.kh405.net/index.php/Accueil.
Toutes vos questions sont passibles d’un "Maintenant OUI", veillez à ne pas
tourner vos questions vers : "Faut-il gérer ... ?"
Les questions devront être écrites en français correct aucun autre langage ne
sera accepté.
Les balises de ce mini-projet sont : [AOC][MP1]"espace" "sujet du problème".
Conseil Ce TP se déroulera sur deux semaines.
Ce n’est pas une raison pour commencer au dernier moment ! !
Messages d’erreur Durant ce projet, plusieurs messages d’erreur différents vous
seront demandées. Vous devrez respecter exactement ces messages d’erreur.
Vous afficherez ces messages sur la sortie standard.
Source des cours Afin de réaliser ce projet, vous allez avoir besoin de quelques
notions. Vous trouverez dans /u/all/bernar_l/public/mp1/ le pdf sur le
cours de Marwan Burelle(Ocaml-cours2.pdf ).
Je rappelle que la commande pour ouvrir un pdf est : 42sh$ xpdf monpdf.pdf &
Makefile Votre Makefile devra fournir les régles suivantes :
- Votre projet devra compiler sur les machines de l’école, sans manipulation
externe. Votre projet devra pouvoir être compilé sans erreurs ni warnings
via la commande gmake clean all
- Une règle glogale eval permettant d’engendrer le binaire (format natif)
eval. Votre projet devra compiler en natif(ocamlopt).
- Une règle clean supprimant tous les résultats de compilation(.cmx, .cmi, le
binaire et les fichiers temporaires).
- Une règle dist dépendant de la règle clean qui fournira une tarball propre
de votre projet.
Restriction Une absence de rendu sera sanctionné d’un − 2.
L’absence ou la non validité de votre fichier AUTHORS sera sanctionné d’un
0.
La triche sera sanctionnée, n’essayez pas de partager vos codes, vous serez
durement sanctionné.
Une tarball sale enlèvera des points a votre note(tarball sale == fichiers temporaires et fichiers binaires présents dans le rendu).
Aucun autre langage que Ocaml ne sera accepté.
Vous devez fournir tous vos fichiers source.
3
Moulinette de test Pour ceux qui utilise déjà des moulinettes de test, vous pouvez
les fournir dans votre tarball de rendu. des points bonus seront attribués si les
tests fonctionnent correctement.
partage de code Je vous rappelle que la politique d’EPITA en ce qui concerne le
partage de code est très stricte. Et comme vous le savez depuis la sup, toute
production de l’esprit ne peut pas être utilisé par quelqu’un d’autre. Tout
partage de code est considéré commme de la triche.
norme Votre projet devra respecter une certaine norme.
Aucune ligne ne devra excéder 80 colonnes.
Pas plus de 25 lignes par fonction.
Vos opérateurs devront toujours avoir des espaces avant et après, après la
virgule vous devrez mettre un espace. De même, il y a un espace après tous
les mots du langage.
Votre code ne contiendra aucun trailing-whitespace.
4
Chapitre 2
Code fourni
Nous vous fournissons les lexer/parser(permettant de traduire une expression de
l’AST) ainsi que le type de l’AST. Le type de l’AST est le suivant :
type value =
Int of int
| Bool of bool
type expr
Value
| BinOp
| UniOp
=
of value
of expr * string * expr
of string * expr
Les opérateurs sont représentés par une chaîne de caractères (c.f string du type
expr), par exemple "+" pour l’addition. Le parser qui engendre l’arbre ne fait aucune
vérification sur les opérateurs (la chaîne peut contenir n’importe quoi) tout ce qui
n’est ni entier, ni booléen sera considéré comme un opérateur.
Le type pour les expressions de t_expr est le suivant :
type t_expr =
TInt | TBool
| TArrow of t_expr * t_expr
Les fichiers fournis sont les suivants :
* ast.mli contenant les déclarations de type précédentes.
* parser.ml(et parser.mly) le parser (produit par ocamlyacc)
* lexer.ml (et lexer.mll) le lexer (produit par ocamllex)
Vous trouverez ces fichiers dans le répertoire :
/u/all/bernar_l/public/mp1 Pour utiliser les types définis dans ast.mli, vous
devrez "ouvrir" le module AST.
Lorsque vous ouvrirez votre fichier.ml, il faudra écrire open Ast sur la première
ligne de votre fichier. De plus, au moment de la compilation, vous devrez faire
dépendre vos fichiers de ast.cmi dans votre Makefile.
5
Chapitre 3
Pretty printing
Il faut écrire une fonction d’affichage d’expression (en utilisant la représentation
des opérateurs dans l’AST) sous forme infixe avec des parenthèses (les opérateurs
binaires seront infixes et les unaires préfixes).
De votre fonction de pretty-printing doit ressortir une expression qui, si elle est
parsée de nouveau produira le même AST. Notamment, après deux passages dans
un couple parser/pretty-printer vous obtiendrez la même expression.
L’utilisation du parser est expliquée plus bas dans le sujet (section 8.3).
Le type de l’AST est le même que celui du type expr.
6
Chapitre 4
Premiers pas de l’évaluation
La première partie de ce mini-projet consiste à coder les différents opérateurs.
Pour chaque opérateur, il faut fournir une fonction travaillant sur les valeurs de nos
expressions (type value).
Si la(les) valeur(s) fournie(s) ne correspond(ent) pas au(x) type(s) attendu(s), la
fonction doit échouer avec assert false.
Les opérateurs demandés sont (minimum) :
* "+","*","-","/" et "mod" : les opérations arithmétiques usuelles (uniquement
sur les entiers). Le "-" peut être binaire ou unaire.
* "&&", "||", "⇒", "⇔", "!=" et "not" : les opérations logiques usuelles (uniquement sur les booléens). Le "not" est unaire. "⇒" correspont à l’implication
logique classique, "⇔" correspond à l’équivalence(l’égalité) et "!=" correspond
au ou exclusif.
* "=", "<>", "<", ">", "≤" et "≥" : les comparaisons usuelles (sur les entiers
uniquement).
* La factorielle "!" (unaire)
* L’exponentielle entière "^"(binaire)
* "odd" et "even" : qui teste la parité d’un entier (rappel : en anglais even =
pair et odd = impair).
Vous pouvez ajouter tous les opérateurs qui vous passent par la tête, mais ceux
situés plus haut sont obligatoires.
On rappelle également que l’évaluation se fait avec des valeurs de type value et
pas directemet avec des int ou des bool. Vous devez donc gérer d’une manière ou
d’une autre l’encapsulation et la désencapsulation.
Vous devez avoir des fonctions qui effectuent désencapsulent, calculent puis retourne un type Value.
7
1. Écrire une fonction d’évaluation pour chaque opérateur demandé.
2. Construire une structure permettant d’associer la string représentant l’opérateur avec la fonction correspodante.
(a) Créer une exception Unbound_operator of string que vous lèverez si
l’opérateur n’existe pas.
(b) Écrire une structure pour les opérateurs binaires.
(c) Écrire une structure pour les opérateurs unaires.
(d) Écrire une fonction pour chaque structure permettant de récupérer la
fonction à partir de la string représentant l’opérateur.
Dans un premier temps, vous pourrez vous intéresser aux listes de couples (ou
listes associatives) pour représenter vos structures.
N’hésitez pas à regarder les modules List dans la documentation OCaml.
8
Chapitre 5
Évaluation
Nous allons maintenant construire l’évaluateur à partir des structures définies
précédemment. Votre fonction d’évaluation prendra en paramètres l’expression (AST)
et les deux structures contenant les opérateurs binaires et unaires (prédemment
créées) et devra renvoyer une valeur de type Value.
La gestion des erreurs sur les opérateurs sera laissée aux structures stockant les
opérateurs.
Vous devez écrire une fonction d’affichage de valeurs (de type value) qui servira
à l’affichage final. Cette fonction respectera les conventions suivantes :
* Si la valeur est un entier, elle affichera : "-:int=val" (où val est la valeur
entière).
* Si la valeur est un booléen, elle affichera : "-:bool=val" (où val est la valeur
booléenne, c’est à dire true ou false).
9
Chapitre 6
Validation et typage
On se propose maintenant de valider les expressions avant de les interpréter. Pour
ce faire, on a besoin d’une algèbre des types. Celle-ci est déjà déterminée par le
type t_expr dans ast.mli. Un type est soit un type de base (entier ou booléen)
soit une construction basée sur la flèche.
Il faut que vous vous référiez au cours de Marwan
(/u/all/bernar_l/public/mp1/Ocaml-cours2.pdf).
type t_expr =
TInt | TBool
| TArrow of t_expr * t_expr
1. Sur le même modèle que pour l’évaluation des opérateurs (mais sans différencier binaire et unaire) construire une structure de recherche qui permet de
retrouver le type d’un opérateur à partir de la chaîne le représentant.
2. Définir l’exception Type_error of expr qui indique une erreur de typage sur
l’expression en paramètre.
3. Écrire la fonction d’application de la flèche qui prend deux types (le premier
devant être une flèche) et applique la flèche passée en paramètre. Si le premier
type n’est pas une flèche ou que le second ne correspond pas au domaine de la
flèche, votre fonction devra lever l’exception Type_error.
4. Écrire la fonction qui vérifie le typage d’une expression à partir de la structure
que vous avez construite pour les opérateurs.
La fonction de typage devra lever l’exception Type_error en cas d’erreur de typage. Si l’opérateur n’existe pas, elle utilisera l’exception Unbound_operator comme
pour l’évaluation.
On rappelle les types des opérateurs :
* Binaires :
* "+", "-", "*", "/", "mod" et "^" : int → int → int
* "&&", "||", "⇒", "⇔" et "!=" : bool → bool → bool
* "=", "<>", "≤" et "≥" int → int → bool
* Unaires :
* "-" et "!" : int → int
* "not" : bool → bool
* "odd" et "even" : int → bool
10
Chapitre 7
Rendu
Cette partie décrit votre rendu, sa structure ainsi que quelques conseils sur l’utilisation des fichiers fournis.
7.1
Utilisation du parser
Le parser construit un AST à partir d’un flux de caractères (via le lexer). Vous
devez donc lui fournir ce flux. Dans ce qui vous est demandé, on lit les expressions
sur l’entrée standard du programme (le terminal, mais aussi un fichier ou la sortie
d’un autre programme avec les redirections et les tubes du shell).
Les expressions lues sont terminées par un retour à la ligne, contenant des entiers (positifs uniquement car les négatifs sont gérés par l’opérateur unaire "-"),
des booléens("true" ou "false"), des parenthèses et des opérateurs (tout ce qui
n’est ni un entier, ni un booléen, ni un espace ou une parenthèse). La fin de fichier
(le caratère envoyé par "ctrl D" dans le terminal) indique la fin du parsing (et du
programme).
Le parser s’utilise de la manière suivante :
(* Création du flux de caractères à partir de l’entrée standard *)
let lexbuf = Lexing.from_channel stdin in
(* On boucle indéfiniment, la fin est indiquée par l’exception Lexer.Eof *)
try
while (true) do
(* Appel du parser et récupération de l’AST dans parse_res *)
let parse_res = Parser.main Lexer.token lexbuf in
(* typage et évaluation *)
... (* FIX ME *)
done
with Lexer.Eof -> exit 0
11
Note sur le parser : Le parser ne connait pas les opérateurs, par conséquent, il
ne connait pas leur priorité. Du coup, ses règles de priorité sont simples :
* Les opérations unaires sont les plus prioritaires.
* Le parenthésage implicite se fait par la gauche.
Par conséquent, l’expression 1 + 1 ∗ 2 sera comprise comme (1 + 1) ∗ 2 et
non pas 1 + (1 ∗ 2).
7.2
Programme principal
* Votre programme principal (eval) lancé sans option devra lire une expression
sur l’entrée standard et afficher le résultat de l’évaluation (comme indiqué
précédemment). Si tout se passe bien (lexing/parsing, typage, évaluation), il
lit une expression(terminée par un retour à la ligne), affiche l’évaluation et
attend l’expression suivante. Lorsque le caractère de fin de fichier arrive (géré
par le lexer), le programme se termine avec un code de retour = 0(exit 0).
* Les erreurs ne sont pas fatales : lorsqu’une expression est incorrecte, le programme doit le signaler (voir la suite) et attendre l’expression suivante.
* Les erreurs liées à la construction des expressions (erreur de syntaxe) sont
gérées par le lexer/parser. Vous ne devez gérer que les erreurs de type.
* Dans le cas d’une erreur au typage, votre programme devra afficher :
Erreur de type dans l’expression: <expr>
Où <expr> doit être remplacé par l’expression posant problème.
* Si l’opérateur n’est pas défini, vous devrez afficher le message suivant :
L’operateur <op> n’existe pas.
Où <op> doit être remplacé par l’opérateur fautif.
* Si l’on passe l’option -print_only à votre programme, il devra se contenter
de réafficher l’expression parsée (une ligne à la fois jusqu’à la fin du fichier).
* Si l’on passe l’option -type_only à votre programme, il devra vérifier le type
de l’expression (une à la fois jusqu’à la fin du fichier), afficher le message
d’erreur déjà décrit le cas échéant ou afficher sur la sortie standard l’expression
suivante :
VALID -:type
Où type est le type déduit de l’expression.
Vous êtes libre d’utiliser le module Arg fourni avec OCaml pour gérer les options de
la ligne de commande.
12
Chapitre 8
Conseil
8.1
Les structures de données
La structure de recherche la plus simple à implémenter (mais la moins efficace)
est la liste associative : une simple liste de couples dont le premier élément est la
clef et le second la valeur.
La fonction List.assoc recherche une clef dans une liste associative et renvoie la
valeur associée si la clef existe, ou lève l’exception Not_found si la clef n’existe pas.
Voici une implémentation non-exhaustive :
let rec assoc x = function
[] -> raise Not_found
| (a,b)::_ when a=x -> b
| _::t -> assoc x ts
Les listes associatives ont une complexité linéaire pour la recherche. OCaml founit
des tables de Hash via le module Hashtbl qui font la recherche en temps constant
(donc beaucoup mieux). La documentation du module est disponible sur le site
d’OCaml. Le module Hashtbl s’utilise relativement simplement : il offre une fonction
de création (Hashtbl.create), une fonction d’ajout (Hashtbl.add) par modification en
place (procédure) et une fonction de recherche (Hashtbl.find) qui renvoit la valeur
associée à une clef ou lève l’exception Not_found si la clef n’existe pas.
8.2
Le typage des opérateurs
L’objectif est de stocker le type associé à chaque opérateur pour le retrouver simplement. On pourrait utiliser 2 structures de recherche (pour les opérateurs unaires
et binaires) mais une seule suffit ici. Certains pourraient se demander comment gérer
les opérateurs qui sont à la fois unaires et binaires... Rien de plus simple, on enregistre les opérateurs en leur ajoutant un suffixe indiquant leur parité (par exemple,
on stocke dans la structure de recherche la clef "-u" à la place de "-" pour le moins
unaire), il suffit de rajouter le bon suffixe lors de la recherche consécutive. Attention,
vous devez malgré tout être capable de dire si un opérateur existe, même s’il est mal
utilisé.
13
Notamment, le typage de l’expression + 1 devra répondre :
Erreur de type dans l’expression: + 1
et non pas :
L’operateur + n’existe pas.
Pour la gestion des erreurs, utilisez intelligemment les exceptions pour afficher
les messages d’erreur au bon moment. Profitez également du fait que vous disposiez
d’une fonction d’affichage d’expression pour vos messages d’erreurs.
8.3
Le parser
Attention, le parser est relativement limité du fait qu’il n’y ait que peu de
contraintes sur les opérateurs. Par conséquent, il ne faut pas hésiter à ajouter espaces ou parenthèses pour lever les ambiguïtés. Les opérateurs sont définis comme
n’importe quelle séquence de caractères ne contenant ni chiffre, ni espace, ni parenthèse, par conséquent, la chaîne "true&&false" est vue comme un opérateur (un
seul bloc).
Cette remarque est valable pour vos exemples comme pour l’affichage des expressions (vous devez pouvoir reparser les expressions affichées par votre pretty-printer).
Si votre pretty-printer est bon, les deux exemples suivants devraient donner le même
résultat (les parenthèses extérieures ne sont pas forcèment obligatoires, à vous de
vous débrouiller) :
> echo "1
(1 + (2 *
> echo "1
(1 + (2 *
+ (2 * 3)" | ./eval -print_only
3))
+ (2 * 3)" | ./eval -print_only | ./eval -print_only
3))
14
Chapitre 9
Exemples
Voici quelques exemples d’intéractions avec votre programme (en construction) :
> ./eval
1 + 1
-:int=2
true && true
-:bool=true
1 + true
Erreur de type dans l’expression: (1 + true)
1 ++ 2
L’operateur ++ n’existe pas.
^D
>
Mettre les expressions dans un fichier :
> cat exemple.txt
1 + 1
true && true
1 + true
1 ++ 2
> ./eval < exemple.txt
-:int=2
-:bool=true
Erreur de type dans l’expression: (1 + true)
L’operateur ++ n’existe pas.
>
15
Un peu de typage :
> ./eval -type_only
1 + 1
VALID -:int
true && false
VALID -:bool
1 + ( true + 2 )
Erreur de type dans l’expression: ( true + 2 )
1 toto 2
L’operateur toto n’existe pas.
1 + 2 + 3 + 4 + true
Erreur de type dans l’expression: ((((1 + 2) + 3) + 4) + true)
1 + 2 + 3 + 4 + ( true + 5)
Erreur de type dans l’expression: (true + 5)
>
Quelques exemples supplémentaires :
> ./eval -print_only < exemples
((2 * ! 5) + (3 mod 2))
(not true && not false)
(true && false)
((not true => not false) <=> (false => true))
(((2 > 1) => (2 > 0)) <=> ((0 <= 2) => (1 <= 2)))
(((2 ^ 16) = (2 * (2 ^ 15))) && (((2 ^ 10) mod 2) = 0))
> ./eval -type_only < exemples
VALID -:int
VALID -:bool
VALID -:bool
VALID -:bool
VALID -:bool
VALID -:bool
> ./eval < exemples
-:int=241
-:bool=false
-:bool=false
-:bool=true
-:bool=true
-:bool=true
>
16