Compilation 2005/2006 |
|||||
French only |
|||||
Partie 5 : Production de codeA présenter lors de la séance du 7 février 2006. Le but de cette partie est de produire le code machine correspondant au programme analysé. Toutes les étapes précédentes avaient pour seul but de s'assurer que le programme était correct par rapport aux règles du langage, et de transformer le programme textuel en une forme facilement manipulable : l'arbre abstrait. Une fois ce travail préparatoire effectué, le compilateur peut commencer avec la production de code cible. Pour les autres parties du projet, on a toujours donné une spécification formelle de ce qu'il fallait implémenter sous forme d'une grammaire lexicale, d'une grammaire syntaxique et de règles de typage. Pour cette partie, on suppose qu'il y a un consensus sur la signification à l'exécution des différents concepts du langage Zwei (méthodes, objets, opérateurs, etc). On se passera donc d'une description formelle (en cas de doute, utilisez le forum de discussion ou l' interpréteur Zwei que nous vous fournissons). La production de code nous occupera jusqu'à la fin du semestre. Nous séparerons cette partie en plusieurs étapes, que nous vous conseillons d'effectuer dans l'ordre donné, mais vous êtes bien entendu libres de vous organiser comme bon vous semble. L'idée générale de la production de code est relativement simple : il s'agit de parcourir l'arbre abstrait et de produire, pour chaque nœud, le code qui lui correspond. La production de code s'organise donc autour du pattern matching (ou des visiteurs en Java). L'implémentation des différents cas des fonctions de génération de code peut être faite par étapes. Celles-ci sont décrites ci-dessous. La description de chaque étape commence par l'énumération des nœuds de la syntaxe abstraite qui doivent être implémentés au cours de l'étape. Étape 0 : Les primitives d'entrée-sortieE = ReadInt | ReadChar | ... S = PrintInt E | PrintChar E |...
La génération de code pour les primitives d'entrée-sortie consiste
simplement à utiliser les appels-système correspondants :
Étape 1 : Les typesT = ClassType name | IntType | NullType
Les occurrences de types ne produisent pas de code, il n'est
donc pas nécessaire de définir la génération de code pour les
nœuds Étape 2 : Les expressions arithmétiquesE = IntLit int | Binop B E E | Unop U E | ... B = Add | Sub | Mul | Div | Mod | ... U = Neg | ...
Le but de la deuxième étape est de produire du code pour les
expressions arithmétiques. À la fin de cette étape, le
compilateur devrait être capable de compiler des programmes
constitués d'une simple expression arithmétique, comme
Étape 3 : L'expression conditionnelleE = If E E E | ... Le but de la troisième étape est d'ajouter la gestion des expressions conditionnelles au compilateur. Étape 4 : Les classes (sans les méthodes) et les objetsP = Program { D } E D = ClassDef name [ name ] { M } | ... M = FieldDecl name T | ... E = New name { E } | Select E name | NullLit | ... Dans un premier temps, on choisit de ne pas s'occuper de la génération de code pour les définitions de méthode et les appels de méthode. Ce travail est décrit plus tard, dans l'étape 8.
Pour chaque champ de la classe, il faut se rappeler de sa
position dans la classe. On stocke donc cette position dans le
champ
Si
Concrètement, un objet est représenté dans la partie "tas" de la
mémoire par une zone contenant un pointeur vers sa table de
méthode virtuelle et les valeurs des champs de l'objet, tout mis
côte à côte. La valeur d'un objet est l'adresse du début de
cette zone mémoire. La génération de code pour une expression
La génération de code pour le nœud de sélection de champ
Il faut noter que toutes les données que nous manipulons dans le langage (les entiers et les [pointeurs vers les] objets) peuvent être représentés en mémoire par un mot.
Pour allouer la mémoire, vous devez utiliser l'appel système
Étape 5 : Les blocs et les variables localesS = Var name T E | Set name E | ... E = Ident name | Block { S } E | ...
Le but de la cinquième étape est d'ajouter la gestion des blocs,
des variables locales et des affectations. Pour cela, il vous
faut gérer la pile. Comme vous l'avez vu au cours, chaque appel
de fonction a le droit d'utiliser un bloc de mémoire, appelé
bloc d'activation, alloué sur la pile. L'adresse du début de ce
bloc ne change pas au cours d'un appel donné; il est désigné par
Lors de la génération de code, vous devez calculer pour chaque
instruction quelle est la taille du bloc d'activation. Pour
cela, la classe
Le nœud
Pour générer le code du nœud
La génération de code des nœuds Étape 6 : Les bouclesS = While E { S } | Do E | ...
Le but de la sixième étape est d'ajouter la gestion des boucles
Étape 7 : Les opérateurs logiques et de comparaisonsE = Unop U E | Binop B E E | ... U = Not | ... B = And | Eq | Ne | Lt | Le | Gt | Ge | ...Remarque: pour comparer deux objets il faut comparer leur référence et non pas leur structure. Étape 8 : Les méthodesM = MethodDef name { name T } T E | ... E = Call E name { E } | ...
Le but de la septième et dernière étape est d'ajouter la gestion
des méthodes au compilateur. Cela inclut le calcul de la table
des méthodes virtuelles pour chaque classe, la génération de
code pour les définition de méthodes et pour les appels de
méthodes. Il vous faudra aussi retoucher la génération de code
pour le nœud Les arguments des méthodes doivent être évalués de gauche à droite avant d'être passés à la fonction.
Le programme de test Pour lancer le programme à partir du répertoire racine de votre projet, utilisez la commande suivante :
scala -cp ./classes zweic.GeneratorTestTest fichier-source [ fichier-destination ]
La sortie est simplement affichée à l'écran si l'argument
Pour le fichier d'exemple
scala -cp ./classes zweic.GeneratorTest
examples/Factorial.zwei
examples/Factorial.asm
Pour cette étape, nous vous fournissons les fichiers suivants : Canevas Scala
Le fichier compressé
Les fichiers fournis avec le canevas Java sont:
Le fichier compressé
Pour tester les programmes produits par votre compilateur, nous
mettons à votre disposition un interpréteur RISC et un débogueur
graphique simple. Ces outils prennent un certain nombre
d'options, dont vous pouvez obtenir la liste en les lançant avec
l'option
L'interpréteur se lance en tapant
Parfois l'exécution d'un programme Zwei échoue à cause d'un dépassement de la mémoire. Il est possible de dire à l'interpréteur d'utiliser une taille plus grande pour la mémoire : risc-emu -mem=1000000 monfichier.risc
Pour utiliser les émulateurs RISC sur votre ordinateur
personnel, vous pouvez télécharger l'une des deux archives
suivantes:
risc-20051207.tgz ou
risc-20051207.zip. Aprés désarchivage, les
exécutables se trouvent dans le répertoire L'architecture utilisée a brièvement été présentée au cours. Les éléments importants sont rappelés ici. Le processeur comporte 32 registres, numérotés de 0 à 31. Quelques registres ont un usage particulier :
Program dans la fonction gen .
Toutes les instructions sont codées au moyen de 32 bits. Certaines parties de ces 32 bits peuvent contenir des constantes, dont il est important de connaître la taille au moment de la production de code. Sur la feuille distribuée au cours et décrivant le jeu d'instruction, ainsi que dans la documentation de l'interface RISC, ces constantes sont identifiées par un nom. La table ci-dessous donne la taille de ces constantes.
|
|