L'objectif du module de compilation est de traduire les
instructions MiniJaJa contenues dans l'arbre de syntaxe abstraire en
instructions JaJaCode.
6.1 Idée générale
Pour réaliser la traduction nous utilisons le paradigme de
programmation visiteur, qui nous permet d'être totalement
indépendants des modifications qui ont lieu sur l'analyseur lexical et
syntaxique.
Nous avons donc un visiteur qui parcourt l'arbre de syntaxe abstraite,
à chaque noeud qu'il rencontre, il effectue les actions mentionnées
dans les règles de traduction du MiniJaJa vers le JaJaCode.
6.2 Réalisation
6.2.1 Choix des structures de données
Le premier problème que nous avons eu a résoudre fut le choix de la
structure de données à adopter pour stocker les instructions
JaJaCode. Nous avons fait ce travail avec l'équipe du module
Interprétation JaJaCode afin de trouver la structure la mieux
adaptée pour la traduction et l'interprétation. L'interprétation du
JaJaCode est linéaire, avec des sauts conditionnels donc du point de
vue de l'interprétation c'est sous forme de tableau d'instructions
qu'il faudrait stocker le JaJaCode, les indices du tableau
représentant les adresses des instructions.
Toutefois le tableau a un inconvénient : sa taille fixe, et il est
impossible de prévoir à l'avance le nombre d'instructions que va
générer la traduction. Du point de vue de la traduction c'est la liste
chaînée qui est la plus facile à manipuler. Java a coupé la poire en
deux puisqu'il met à disposition le type Vector qui permet
l'utilisation de tableaux non bornés. C'est naturellement cette
structure de données que nous avons choisi d'utiliser.
6.2.2 Le paradigme de programmation visiteur
Ce paradigme a été utilisé dans beaucoup de modules du projet. Nous
allons voir en quelques lignes en quoi il consiste.
Le principe du motif Visiteur consiste à réaliser une classe agissant
sur les données issues d'autres classes. Imaginons un ensemble de
classes dérivées d'une classe mère, chacune implémentant une méthode
draw(). Le principe du Visiteur va consister à déplacer toutes les
méthodes draw() dans le Visiteur pour ne pas dupliquer de code entre
les différentes classes.
La réalisation d'une opération de visite se déroule suivant un
protocole particulier. Chaque classe susceptible de se voir visitée
doit contenir une méthode intitulée accept(Visitor v). Le code de
cette méthode exécutera en retour l'instruction v.visit(this) pour
signifier au Visiteur qu'il possède le droit de venir voir ce qu'il se
passe dans cette classe.
6.2.3 Gestion des adresses
La gestion des adresses est modifiée par rapport au règles de
traduction. En effet, dans les règles, les instructions manipulant des
adresses (if et goto) se basent sur l'adresse n qui représente
l'adresse de début de la règle courante. Cependant les instructions de
branchement ont besoin de connaître le nombre d'instructions générées
par l'analyse de noeud qui interviennent après le branchement.
Par exemple:
[ctantque]: n |- e Þ {pe, ne}, n+ne+2 |- iss Þ
{piss, niss}/n |- tantque(e, iss) Þ {((pe ÅD not) ÅD
(if(n + ne + niss + 3)) Å piss ÅD goto(n)), ne + niss + 3
}
Dans cette règle on a l'instruction if qui est ajoutée à la
liste d'instructions avant les instructions piss. Or if
a besoin de niss, le nombre d'instructions associées à piss, et il ne
peut l'avoir à ce moment là puisqu'il ne sera connu qu'après
l'analyse du noeud de l'ASA correspondant (iss).
Pour résoudre ce
problème, nous appliquons le proverbe Il faut remettre à plus tard
ce que l'on peut faire maintenant. C'est à dire qu'au moment
d'ajouter l'instruction if à la liste, c'est une instruction
nop qui est ajoutée (on prend alors soin de sauvegarder
l'emplacement de cette instruction). Puis, lorsque l'on a toutes les
informations pour constituer le if, on remplace l'instruction
nop à l'adresse sauvegardée par le if dûment
rempli. Dans notre cas c'est après l'analyse du noeud iss que l'on
procède à l'opération. A ce moment là, l'adresse de l'instruction
courante est n + ne + niss + 2, donc on remplace le nop par un
if(adresse_instruction_courant + 1).
6.2.4 Gestion des retraits
Pour gérer les règles de retraits, nous passons en argument de la
fonction de visite un tag , qui est une chaîne de caractères
qui indique au noeud appelé qu'il doit utiliser les règles de retrait
pour ce noeud au lieu des règles traditionnelles.
Exemple de méthode :
public Object visit(ASTcst node, Object data) {
if((String) data == "retrait") {
// swap
vectjjc.addElement(new JJCSwap());
// pop
vectjjc.addElement(new JJCPop());
}
else {
// t - type
String type = (String) node.jjtGetChild(0).toString();
// i - identificateur
String ident = (String) node.jjtGetChild(1).toString();
// pe - expression
node.jjtGetChild(2).jjtAccept(this,data);
// new(i, t, cst, 0)
vectjjc.addElement(new JJCNew(ident, type, "cst", 0));
}
return null;
}
Nous avons ici un noeud (cst) qui a une règle de retrait. Si
la chaîne de caractères retrait est passée en paramètre de la méthode
ce seront les instructions de retrait qui seront ajoutées à la liste
d'instructions JaJaCode. Sinon c'est la règle normale qui
s'applique.
Exemple d'appel :
public Object visit(ASTclasse node, Object data) {
// init
vectjjc.addElement(new JJCInit());
// pdss - liste de declarations
node.jjtGetChild(1).jjtAccept(this, data);
// pmma - methode main
node.jjtGetChild(2).jjtAccept(this, data);
// prdss - retrait liste de declarations
node.jjtGetChild(1).jjtAccept(this, "retrait");
// pop
vectjjc.addElement(new JJCPop());
// jcstop
vectjjc.addElement(new JJCJcstop());
return null;
}
Dans cette exemple on voit que pour retirer les déclarations dans le
noeud classe on demande le parcours du noeud déclarations
mais avec comme paramètre la chaîne de caractères retrait
6.2.5 Problème du un noeud - deux règles
Ce problème est de même nature que celui du retrait. Il provient du
fait que l'arbre de syntaxe abstraite ne fait pas la différence entre
certains noeuds. Par exemple pour une affectation, l'ASA renvoie un
noeud ASTaffectation pour les variables et les tableaux. Or, le
compilateur n'effectue pas les mêmes actions s'il doit traduire une
affectation de variable ou de tableau. Afin de faire la différence
entre les deux cas nous utilisons la méthode Java instanceOf
qui nous indique quel est le type du noeud que nous traitons et donc
nous permet d'appliquer les règles adéquates lorsque l'ASA n'est plus
assez précis.
6.2.6 Traitement des erreurs
Le module de compilation ne génère pas d'erreurs. En effet, il se
limite à l'application de règles qui transforment un code en un autre. Il n'a cependant pas de moyens de savoir si ces codes sont justes
ou faux. S'il y a un problème syntaxique il sera levé lors de
l'analyse lexicale et syntaxique. S'il y a un problème sémantique il
ne pourra apparaître que lors de l'interprétation JaJaCode.
6.3 Jeux de test
Les tests effectués sur le module compilateur sont les suivants :
Tests sur la génération de code :
-
Test de toutes les instructions indépendamment
- Test de toutes les instructions dans un seul programme
Tests sur les structures de données utilisées :
-
Test avec un volume important d'instructions.
- Test sur les fonctions particulières du type Vector
(fonctions addElement pour ajouter un élément au bout d'un vecteur,
et set pour remplacer un élément dans le vecteur.