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