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 !
Nous détaillons maintenant la syntaxe exacte du langage.
%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();
}
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.
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.
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.
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 !
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.
Il est demandé que dans l'implantation du projet existe des modules différents pour ses composants suivants:
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é.
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.
Les critères de notation sont :
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...
| 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 |