Projet de Traduction - LOIR: Langage Objet Interprété et Reflexif


DATE DE REMISE: repoussé au 23 mai 2010 minuit
SOUTENANCES: 2010

Vue générale du projet

Le but du projet est d'écrire un petit interprète d'un langage particulier, contenant des objets, des classes et de l'héritage. Ce langage comporte plusieurs différences fondamentales avec Java, dont voici les principales:

Comme en Java:

Voici un exemple de code à interpréter :

% rem Définition d'une classe Integer
class Integer {
  int value;
  Integer(int v) {
    value:=v;
  }
  int changeValue(int newValue) {
    var int oldValue;
    oldValue:=value;
    value:=newValue;
    return oldValue;
  }
  int getValue() {
    return value;
  }
  int add(int v) {
    value:=value+v;
    return value;
  }
  boolean isPrime() { ... }
}
% rem Création d'un objet de type Integer avec appel du constructeur "Integer(3)"
% rem L'opérateur new appelle un constructeur. Entre les crochets, le nombre de paramètres
% a:=new Integer(3)
% rem Affichage de toutes les variables définies (nom:type:appel de toString() sur l'objet)
% env
a:Integer:[value(int):3]
% b:=a.changeValue(2)
% env
a:Integer:[value(int):2]
b:int:3
% rem Définition d'une classe qui hérite de Integer et d'un sous-type de Integer
% rem un champ est ajouté
% rem le constructeur de Integer est exécuté avant ExtendedInteger, avec les arguments indiqués
% rem la méthode int getValue() héritée est redéfinie
% rem la méthode void changeValues(int, int) est ajoutée
% rem la méthode int add(int) héritée est supprimée (on ne peut supprimer que des méthodes dont on devrait hériter (c.f. grammaire)) !
class IntegerSum extends Integer {
  int additionalValue;
  IntegerSum(int v1, int v2) precalls Integer(v1) {
    additionalValue:=v2;
  }
  int getValue() {
    return value+additionalValue;
  }
  void changeValues(int newValue1, int newValue2) {
    changeValue(newValue1);
    additionalValue:=newValue2;
  }
  remove int add(int v)
}
% rem On récupère un objet Introspector associé à a
i:=System.getIntrospector(a)
% rem On peut récupérer un objet Method
m:=i.getMethod("int add(int)")
% rem On peut même récupérer le code de la méthode
% rem et jouer avec (la classe suivante est incomplète: tout n'est pas écrit) !
% class MyVisitor {
    void visit(IMethod m) {
      var IBloc b;
      var charsequence s;
      var int i;
      s:=m.getDescriptor();
      System.printf(s);
      System.printf("\n");
      i:=m.getNumberOfParameters();
      var int j;
      while (j<i) {
        var IVar v;
        System.printf("Paramètre ");
        System.printf(i);
        System.printf(": ");
        v:=m.getParameter(j);
        v.accept(this);
        System.printf("\n");
      }
      b:=m.getCode();
      b.accept(this);
    }
    void visit(IBloc s) {
      var int i;
      var int nb;
      System.printf("Suite d'instructions:\n");
      nb:=s.getNumberOfInstructions();
      i:=0;
      while (i<nb) {
        var IInstr ins;
        System.printf("Instruction ");
        System.printf(i);
        System.printf(": ");
        ins:=s.getInstr(i);
        ins.accept(this);
        System.printf("\n");
      }
    }
    void visit(IMethodCall mc) {
      var int i;
      var int nb;
      System.printf("Appel de la méthode ");
      System.printf(mc.getName());
      System.printf("Paramètres:");
      nb:=s.getNumberOfParameters();
      i:=0;
      while (i<nb) {
        var IExpr exp;
        System.printf("Paramètre ");
        System.printf(i);
        System.printf(": ");
        expr:=s.getArg(i);
        expr.accept(this);
        System.printf("\n");
      }
    }
    void visit(IAff aff) {
      var IExpr lvalue;
      var IExpr rvalue;
      System.printf("Affectation de ");
      lvalue:=aff.getLValue();
      lvalue.accept(this);
      System.printf(" qui reçoit ");
      rvalue:=aff.getRValue();
      rvalue.accept(this);
      System.printf("\n");
    }
    void visit(IAdd v) {
      var IExpr fg;
      var IExpr fd;
      System.printf("Somme de ");
      fg:=v.getLeftChild();
      fg.accept(this);
      System.printf(" et de ");
      fd:=v.getRightChild();
      fd.accept(this);
      System.printf("\n");
    }
    void visit(IVar n) {
      System.printf("Variable de nom ");
      System.printf(n.getName());
      System.printf("\n");
    }
    void visit(IReturn r) {
      var IExpr r;
      System.printf("Return ");
      r:=r.getReturnValue();
      r.accept(this);
      System.printf("\n");
    }
  }
% v:=new MyVisitor()
% m.accept(v)
int add(int)
Paramètre 0: Variable de nom v
Suite d'instructions:
Instruction 0: Affectation de Variable de nom valeur
 qui reçoit Somme de Variable de nom valeur
 et de Variable de nom v

Instruction 1: Return Variable de nom valeur
%
% rem Sauvegarde de l'environnement dans le fichier "toto"
% save "toto"
% quit
Goodbye !

Vues techniques

Nous détaillons maintenant la syntaxe exacte du langage.

Analyse syntaxique des classes

La grammaire des classes, qui est LALR(1), vous est donnée ici par une description YACC. Vous pouvez la changer, à condition de preserver la syntaxe: n'importe quelle classe définie avec votre grammaire modifiée doit également pouvoir l'être avec celle qui vous est donnée. Voici la signification syntaxique des terminaux utilisés dans la grammaire: Et la grammaire :
%token CLASSE NOM INT DOUBLE CHARSEQUENCE BOOLEAN
%token ICONST, DCONST, CCONST, BCONST  
%token IF WHILE ELSE VAR RETURN STATIC EXTENDS PRECALLS REMOVE PRINTF GETINTROSPECTOR

%%

class: CLASSE NOM '{' membres '}'
     | CLASSE NOM '{' '}'
     | CLASSE NOM EXTENDS NOM '{' membres '}'
     | CLASSE NOM EXTENDS NOM '{' '}' 
;

membres: membre
       | membre membres
;

membre: champ | methode | constructeur 
      | REMOVE type NOM '(' params ')' | REMOVE type NOM '(' ')'
;

champ: type NOM ';'
;

type: INT | DOUBLE | NOM | CHARSEQUENCE | BOOLEAN
;

constructeur: NOM '(' params ')' bloc
            | NOM '(' ')' bloc
            | NOM '(' params ')' PRECALLS NOM '(' lparams ')' bloc
            | NOM '(' params ')' PRECALLS NOM '(' ')' bloc
            | NOM '(' ')' PRECALLS NOM '(' lparams ')' bloc
            | NOM '(' ')' PRECALLS NOM '(' ')' bloc
;

methode: type NOM '(' params ')' bloc
       | type NOM '(' ')' bloc
;

params: type NOM
      | type NOM ',' params
;

bloc: '{' '}'
    | '{' instrs '}'
;

/* Si on n'utilise pas VAR ici on a un conflit: quand l'analyseur rencontre
   un nom après une déclaration, il ne peut pas décider s'il s'agit d'un nom
   de variable pour une nouvelle déclaration ou pour une expression
*/
decl: VAR type NOM ';'
;

instrs: instr
      | instr instrs
;

/* Une règle sans ELSE provoque un conflit: on ne sait pas 
   sur quoi porte le ELSE
*/
instr: bloc
     | IF '(' expr ')' instr ELSE instr
     | WHILE '(' expr ')' instr
     | lvalue ':' '=' expr ';'
     | expr ';'
     | RETURN ';'
     | RETURN expr ';'
     | PRINTF '(' expr ')' ';'
     | decl ';'
;

lvalue : expr
;
/* Il faut éventuellement modifier la grammaire pour que les opérateurs aient leur
   priorité normale
*/
expr: exprAtom '+' expr
    | exprAtom '-' expr
    | exprAtom '/' expr
    | exprAtom '*' expr
    | exprAtom '=' expr
    | exprAtom '<' expr
    | exprAtom '<' '=' expr
    | exprAtom '>' expr
    | exprAtom '>' '=' expr
    | exprAtom 'a' 'n' 'd' expr
    | exprAtom 'o' 'r' expr
    | 'n' 'o' 't' expr
    | '+' expr
    | '-' expr
    | exprAtom
;

exprAtom: NOM
    | ICONST | BCONST | CCONST | DCONST
    | NOM '(' lparams ')'
    | NOM '(' ')'
    | NOM '.' NOM '(' lparams ')'
    | NOM '.' NOM '(' ')'
    | NOM '.' NOM
    | 'n' 'e' 'w' ' ' NOM '(' lparams ')'
    | 'n' 'e' 'w' ' ' NOM '(' ')'
    | GETINTROSPECTOR '(' expr ')'
    | '(' expr ')'
;

lparams: expr
    | expr ',' lparams
;
%%

On doit pouvoir mettre des caractères d'espacement aux bons endroits: ils n'apparaissent pas dans la grammaire, à vous de les ajouter.

L'analyseur syntaxique devra construire, à partir de la description de la classe, un Abstract Syntaxt Tree (AST) décrivant la classe sous la forme d'un arbre. Les noeuds de l'arbre ont des types. Voici, décrits sous la forme d'interfaces Java, la hiérarchie des types des noeuds de l'AST ainsi que leurs fonctionnalités (on donne également des interfaces pour des types qui ne sont pas des noeuds, mais qui sont nécessaires pour la description de l'arbre):

interface Visitable {
  void accept(Visitor v);
}
interface IMember extends Visitable {}
interface IField extends IMember {
  const char* getName();
  IType getType();
  IExpr getValue();
}
interface IMethod extends IMember {
  const char* getDescriptor();
  int getNumberOfParameters();
  IVar getParameter(int number);
  IBloc getCode();
}
interface IConstr extends IMember {
  const char* getDescriptor();
  int getNumberOfParameters();
  IVar getParameter(int number);
  IBloc getCode();
  IMethodCall getPreCall(); // null si pas de precalls défini
}
interface IInstr extends Visitable {}
interface IBloc extends IInstr {
  int getNumberOfInstrs();
  int getNumberOfLocalVars();
  IVar getLocalVar(int number);
  IInstr getInstr(int number);
}
interface IIf extends IInstr {
  IExpr getCond();
  Instr getInstr();
  Instr getElse();
}
interface IWhile extends IInstr {
  IExpr getCond();
  Instr getInstr();
}
interface IAff extends IInstr {
  IExpr getLValue();
  IExpr getRValue();
}
interface IReturn extends IInstr {
  IExpr getReturnValue();
}
interface IPrintf extends IInstr { // Pour System.printf
  IExpr getValue();
}
interface IExpr extends Visitable {
  IExpr getValue(); // Evalue l'expression, retourne la valeur
  IType getType();  // Evalue le type de l'expression et le retourne
}
interface IVar extends IExpr {
  const char* getName();
}
interface IConst extends IExpr {
}
interface IBinOp extends IExpr {
  IExpr getLeftChild();
  IExpr getRightChild();
}
interface IAdd extends IBinOp {} // Même chose pour les autres opérateurs binaires.
interface IUnOp extends IExpr {
  IExpr getChild();
}
interface INot extends IUnOp {} // Même chose pour les autres opérateurs unaires.
interface IMethodCall extends IExpr {
  IExpr getCallee();
  const char* getMethodCalled();
  int getNumberOfParameters();
  IExpr getParameter(int number);
}
interface INew extends IExpr {
  IConstr getConstructorCalled();
  int getNumberOfParameters();
  IExpr getParameter(int number);
}
interface IType {
  IType getSuperType();
  int getNumberOfSubTypes();
  IType getSubType(int number);
  const char* getName();
}

Interface d'introspection

Les objets de la classe Introspector permettent d'examiner n'importe quel objet de l'espace mémoire du programme LOIR. Ils contiennent les méthodes suivantes: Tous les types utilisés dans la descriptions des AST (c'est-à-dire les types donnés par les interfaces ci-dessus) doivent pouvoir être réifiés dans l'application. Par ailleurs, chaque noeud de l'AST, et plus généralement chaque composant de l'AST, doit également pouvoir être réifié dans l'application. Les objets chaîne de caractères sont réifiés comme des valeurs de type primitif LOIR charsequence. Par exemple, depuis l'application, la méthode getName() d'un objet de type IType retourne une valeur de type charsequence.

Description de l'interprète

Commandes

Nous présentons dans cette section les différentes commandes proposées par l'interprète. Elles sont présentées par domaine d'activité. Parmi elles, voici celles qui ne sont pas classables dans un domaine d'activité particulier.

Nous présentons maintenant les autres.

Expressions, instructions et affectations

Toute expression écrite dans la syntaxe utilisée pour la définition des classes doit pouvoir être évaluée en ligne de commande. Toute instruction donnée dans la syntaxe utilisée pour la description des classes doit également pouvoir être exécutée.

Les affectations du type nomDeVariable:=expression doivent pouvoir être exécutées. On ne doit pas avoir besoin de déclarer la variable membre gauche de l'affectation. L'interprète doit vérifier la correspondance entre le type du membre gauche et celui du membre droit.

Contexte (Environnement)

L'environnement est l'objet (qui devra être représenté par une structure) qui contient les variables, leurs types et leurs valeurs.

Les commandes agissant sur l'environnement sont les suivantes :

La sauvegarde des différents objets de l'environnement sera réalisée par simple sérialisation. Tous les objets sérialisables devront être sauvés. Pour chacun des autres un message d'erreur sera affiché. Le message sera suffisament explicite pour permettre à l'utilisateur de connaître l'objet qui n'a pas pû être sauvé. Par exemple, si l'objet qui n'a pas pû être sauvé est celui désigné par une variable d'environnement a, alors le programmeur doit être averti que la valeur de la variable a ne pourra pas être restorée dans une session ultérieure.

Appels de méthodes

Il peut exister dans un objet plusieurs méthodes de même nom: c'est une surcharge. Il faut dans ce cas chercher la méthode avec les types de paramètres les plus proches de ceux des arguments de l'appel. Il faut donc implanter l'algorithme de résolution des méthodes que vous avez vu en cours.

Gestion des erreurs

Des erreurs peuvent survenir au cours de l'exécution de l'interprète: erreurs d'analyse syntaxique ou erreurs au cours de l'exécution. Dans ce cas un message d'erreur clair devra s'afficher, indiquant la nature et la localisation de l'erreur. Après affichage du message, la main doit être rendue à l'utilisateur afin qu'il puisse continuer à utiliser l'interprète. Après l'erreur, l'interprète doit être laissé dans l'état le plus propre possible: par exemple, si l'erreur survient pendant une analyse syntaxique et qu'une partie de l'AST a déjà été construite, inutile de conserver cette dernière !

Ce qu'il vous est demandé

Il vous est demandé d'implanter un programme qui réalise ce qui est décrit ci-dessus. La syntaxe qui devra être employée par l'analyseur syntaxique de votre programme devra être rigoureusement identique à celle qui est proposée dans le sujet. En effet, lors de la soutenance, des essais pourront être fait à partir de fichiers d'entrée préconstruits fournis par l'examinateur: votre programme devra fonctionner avec.

Implantation

Il est demandé que dans l'implantation du projet existe des modules différents pour ses composants suivants:

et pour d'autres éventuellement.

Documentation

Votre projet devra être rendu avec deux rapports au format PDF (que vous imprimerez en plus de fournir la version electronique). Le premier (rapport de développement) explique la structure de votre programme, les types de données employés, et les fonctions de manipulation de ces types, avec éventuellement leur complexité, et tout ce que vous jugerez nécessaire. Le second rapport (rapport utilisateur) contient la documentation utilisateur de l'application (pas de listing): options, exemples d'exécutions, etc. Il comportera une introduction, expliquant rapidement le projet, et une conclusion.

Votre rapport utilisateur devra contenir une discussion sur LOIR. Vous y décrirez les avantages de ce langage à base d'objets et ses inconvénients, et le comparerez avec un langage à base de classes comme Java.

En plus des rapports papier, votre code devra être correctement documenté.

Rendre le projet

Votre projet devra être rendu le voir en début de page à minuit au plus tard, par courrier électronique, à votre chargé de travaux dirigés. Le sujet du courier devra être "Projet LOIR". Des filtres pouvant être appliqués sur les boîtes aux lettres des chargés de travaux dirigés, il est important de bien respecter ce sujet.

Le projet sera rendu sous forme de tar.gz (en pièce jointe du courrier électronique. Le nom du fichier tar.gz sera de la forme Nom1Prenom1Nom2Prenom2.tar.gz). Le tar.gz contiendra:

Le projet devra être réalisé par binôme (au plus deux personnes). Il n'y aura pas de dérogation à cette règle. Les deux personnes participent à part égale au projet. Si le correcteur a un doute sur la compétence d'un élément du binôme sur le projet, il pourra mettre deux notes distinctes à chaque personne. Si un même projet est rendu par n binômes différents sa note est 0 pour tout ces binômes.

Notation

Les critères de notation sont :

Le poids de ces différents critères est déterminé par les correcteurs, il est bien entendu identique pour tout les projets. Les aspects qualitatifs du code sont très importants : il ne faut pas seulement présenter un programme qui fonctionne, mais en plus qui est bien conçu au niveau architecture, documentation et clarté du code.

Options

Il est très important, avant de commencer à implanter des options, que la partie obligatoire du projet soit implantée, fonctionne bien, et soit bien documentée. En particulier, l'exemple fourni dans le sujet doit fonctionner.

Une option possible à implanter consiste à prendre en compte les conversions explicites ou implicites, et les promotions entières, comme le fait le compilateur.

Mais vous pouvez aussi laisser libre cours à votre imagination...

Valid HTML 4.01!


N i c o l a s . B e d o n [at] u n i v - m l v . f r - Wednesday, 12-May-2010 11:04:13 CEST