Chapter 8 Interprétation MiniJaJa
L'objectif de l'interpréteur MiniJaJa est d'interpréter le code
MiniJaJa préalablement transformé en Arbre de Syntaxe Abstraite (ASA)
par le parser MiniJaJa.
8.1 Idée générale
8.1.1 Utilisation de l'ASA et application des règles d'interprétation
Afin d'interpréter un code MiniJaJa, il est nécessaire d'utiliser un
arbre de syntaxe abstraite. Si ce code est lexicalement et
sémantiquement correct, c'est-à-dire que l'ASA aura pu être généré
correctement, il suffit qu'il n'y ait pas d'erreur de typage pour
qu'il puisse être interprété. C'est pourquoi, une fois l'ASA généré,
il doit subir le contrôle de type.
Sur cet ASA, il sera possible d'appliquer toutes les règles
d'interprétation étudiées pendant les cours de compilation. Ces règles
doivent s'appliquer sur chaque noeud de l'arbre concerné par une
règle d'interprétation. Plus globalement, cette interprétation est
basée sur un parcours en profondeur de l'ASA.
8.1.2 Objectif de l'interpréteur
L'objectif de l'interpréteur MiniJaJa est de voir le résultat de
l'exécution d'un code MiniJaJa. Toutefois, le langage MiniJaJa ne
permet pas de faire d'entrées/sorties sur les périphériques
d'entrées/sorties standards. Il est donc nécessaire pour constater le
bon fonctionnement du code à interpréter de visionner l'état de la
mémoire pour observer les modifications des variables éventuellement
déclarées. Étant donné qu'à la fin de l'interprétation le contexte de
la mémoire doit être complètement libéré (ie : la pile et le tas doivent
être vides), il est nécessaire de regarder l'évolution de l'état
mémoire pendant l'interprétation. C'est pourquoi nous avons dû mettre
en place un système de points d'arrêt.
8.2 Réalisation
Un certains nombre de points particulièrement intéressants nous ont
obligés a faire des choix techniques lors du développement, c'est ce
que nous allons détailler maintenant.
8.2.1 Parcours de l'ASA, paradigme visiteur
Nous avons vu que le développement de l'interpréteur MiniJaJa reposait
essentiellement sur le développement du parser générant l'ASA. Il
aurait été tout-à-fait envisageable d'intégrer le développement de cet
interpréteur dans celui du parser étant donné que le travail
s'effectue sur la même structure. Toutefois, une des contraintes du
développement de l'ASA est que chaque fois qu'une amélioration y est
apportée, il faut régénérer à l'aide des outils JJTree et JavaCC
les fichiers constituant le parser. Or c'est sur ces mêmes fichiers
qu'il nous fallait travailler pour développer l'interpréteur
MiniJaJa. Au vu de ces contraintes, nous avons choisis d'utiliser le
paradigme de programmation visiteur. Grâce à cela, le développement
des deux parties a pu être complètement indépendant et les évolutions
de l'ASA ont pu être possibles même après le commencement du
développement de l'interpréteur MiniJaJa. L'utilisation de cette
méthode nécessite l'implantation de l'interface ParserVisitor
générée lors de la génération du parser MiniJaJa. Le fonctionnement du
paradigme visiteur est détaillé dans le chapitre compilation (cf. 6.2.2 page ??).
8.2.2 Utilisation la pile
Pendant l'interprétation, la pile est constamment modifiée, que ce
soit pour déclarer des méthodes ou des variables ou encore pour
modifier ces dernières. Ces ajouts, consultations ou modifications
dans la pile sont basées sur les méthodes publiques disponibles dans
la classe EtatMemoire. Les modifications de tableaux, donc dans
les tas, sont transparentes à l'utilisation et sont gérées directement
par la pile.
8.2.3 Retraits dans la pile
Durant l'interprétation d'un programme MiniJaJa, la pile est utilisée
pour stocker les variables et les fonctions. Cela dit, lorsqu'un
élément de pile est devenu inutile il faut le supprimer. Par exemple
après l'exécution d'une fonction, il faut supprimer les variables
locales. De même, à la fin programme, il faut supprimer de la pile
toutes les déclarations de variables, les déclarations de fonctions et
la déclaration de la classe. Ces retraits s'effectuent à la fin des
noeuds classe, appelI et appelE, en utilisant
la méthode retirerDecl() de la pile.
Dans un programme MiniJaJa, la déclaration des fonctions est placée
avant l'exécution du programme principal. Aussi, il ne faut pas
exécuter ce code avant que la fonction soit appelée sinon le résultat
sera incorrect. La solution technique employée pour résoudre ce
problème est qu'au moment de la déclaration de la fonction, le noeud
correspondant est empilé afin d'être réutilisé au moment de
l'appel.
Au moment de l'appel d'une fonction, c'est-à-dire lorsque l'on rencontre à l'interprétation le noeud appelI ou appelE, il faut effectuer les actions suivantes :
-
déclarer les variables de l'entête;
- faire correspondre les valeurs passées en paramètre de la
fonction avec les variables de l'entête déclarées;
- déclarer les variables locales;
- exécuter le corps de la fonction;
- retourner une valeur dans le cas du noeud appelE;
- effectuer le retrait des variables de l'entête et des variables locales.
Il y a une limitation dans l'utilisation des fonctions avec retour. En effet, si la fonction comporte plusieurs
instructions return, l'interpréteur rendra un résultat erroné
puisqu'il fera le retour seulement avec la dernière instruction
return. Ainsi le code suivant :
if (a==1) {
return 1 ;
}else{
return 2 ;
} ;
Doit être remplacé par un code de la sorte :
if (a==1) {
temp=1 ;
}else{
temp=2 ;
} ;
return temp ;
8.2.5 Choix du bon fils
Dans certains cas, il faut savoir quelle règle utiliser étant donné
qu'à un noeud peut correspondre deux règles d'interprétation. C'est
le cas des noeuds somme et affectation. Il est en effet possible de
les utiliser soit avec des variables simples, soit avec des vecteurs.
Il faut donc anticiper et regarder si le fils concerné du
noeud affectation ou somme est de type tableau ou variable. Pour
cela, il y a deux méthodes. La première consiste à afficher le noeud
fils en chaîne de caractères et à regarder la valeur retournée. Cette
valeur correspond au nom du noeud dans l'ASA. Par exemple :
if ((node.jjtGetChild(0).toString())=="tab"){
//code a exécuter
}
Dans cet exemple on regarde si le fils est un noeud tab, ce
qui nous assure que le traitement à effectuer par la suite concerne un
tableau.
Il est aussi possible d'utiliser la méthode Java instanceOf qui
permet de voir si un objet est une instance d'une classe. L'exemple
précédent donnerait le code suivant :
if ((node.jjtGetChild(0) instanceOf ASTtableau){
//code à exécuter
}
8.2.6 Récupérer l'identifiant ou la valeur d'une variable
Un problème qu'il est fréquent de rencontrer est que pour une
variable on veuille récupérer son identificateur et d'autres fois sa
valeur. Pour résoudre ce problème, nous utilisons le second champ
(data) de la méthode visit() (cette méthode provient de
l'implantation de l'interface ParserVisitor). En effet, lorsque
nous voulons que le fils retourne l'identificateur nous passons le
paramètre Integer(0) et Interger(1) s'il nous faut la
valeur.
8.2.7 Points d'arrêt
Pour visionner correctement l'état mémoire en cours d'interprétation,
il a fallu développer un système de points d'arrêt. Pour cela nous
avons choisi d'utiliser le système des threads. Le principe est que
la classe contenant l'interpréteur MiniJaJa est une classe héritant de
la classe Java Thread, ainsi il est possible de suspendre et de
redémarrer l'exécution du processus.
Les points d'arrêt sont matérialisés dans chaque noeud de l'ASA par
une variable booléenne. Cette variable est positionnée à vrai ou à
faux au moment de l'instanciation de la classe contenant
l'interpréteur MiniJaJa. En effet, l'interface passe en paramètre une
liste de numéros correspondant aux lignes où il faut mettre un point
d'arrêt et ensuite l'interpréteur lance une méthode qui va parcourir
l'ASA et positionner la valeur booléenne aux bons endroits.
Tous les noeuds ne sont pas concernés par les points d'arrêt. En
effet, il n'y a pas d'intérêt à mettre un point d'arrêt sur un noe
ud qui concerne plus d'une ligne dans le programme MiniJaJa ou encore
sur un noeud terminal. C'est par exemple le cas des noeuds
decls, vars ou encore enil.
De même, il ne faut pas mettre plusieurs points d'arrêt sur une même
ligne. La solution ici employée est de mettre le point d'arrêt sur le
noeud de plus haut niveau dans l'ASA.
Au moment de l'interprétation du programme, si un point d'arrêt est
trouvé, il faut effectuer les opérations suivantes :
-
retirer le point d'arrêt du noeud (au vu d'une utilisation
ultérieure de l'ASA);
- positionner le booléen threadSuspended à vrai;
- attendre jusqu'à ce que l'interface positionne le booléen
threadSuspend à faux avec la méthode resumeThread().
L'attente de l'événement de l'interface est réalisé dans une boucle
tant que . Cependant, pour éviter de surcharger la machine, un sleep de 20 ms est utilisé à l'intérieur de la boucle. Le délai de 20 ms permet un bon équilibre entre le temps de réaction vis-à-vis de l'interface et la charge de la machine.
L'utilisation de cette méthode pour suspendre l'exécution du thread est préférable à l'emploi des méthodes suspend() et resume() qui sont dépréciées depuis la version 1.1 du JDK et qui peuvent rendre le système instable.
8.3 Jeux de test
Les tests ont été concentrés sur des programmes MiniJaJa contenant des instructions qui modifient ou utilisent la mémoire, qui font des boucles ou encore qui appellent des fonctions. Les noeuds de l'ASA concernés sont donc : classe, tableau, main, cst, var, methode, affectation, affectationT, increment, incrementT, appelI, appelE, retour, somme,sommeT, si, tantque, non, moins, op2.