CSC4251_4252 — Compilation : du langage de haut niveau à l'assembleur

Portail informatique

Compilateur MiniJAVA vers MIPS

La documentation commune aux différentes étapes du projet est regroupée dans la page Mémento du projet MiniJAVA.

Prologue : Prise en main

  • Récupérer le squelette de départ du code du projet :
    $ cd $HOME/CSC4251_4252 $ git clone git@gitlabense.imtbs-tsp.eu:csc4251_4252/csc4251_4252-minijava.git $ cd csc4251_4252-minijava $ ls Docs lib pom.xml readme.md src
  • Si besoin revoir le prologue des exercices JFlex+CUP pour la mise en œuvre de l'IDE Eclipse.
  • Intégrer l'arborescence comme un projet Maven dans votre IDE.
  • Vérifier que le projet est en mode UTF8 pour votre IDE (pas de problèmes d'accents) : dans Eclipse, Window > Preferences > Workspace > Text file encoding à Default (UTF-8).
  • Lire en détails, la section « 1. Structuration des sources » du Mémento MiniJAVA et regarder les classes fournies.
  • Tester la génération (Run As > mvn generate-sources) et l'exécution de la classe principale main.Compiler du compilateur (Run As > mvn install)
    N.B. : L'utilisation de Maven en ligne de commande reste fonctionnelle mais peut poser des problèmes si l'IDE est actif en même temps. Pour l'exécution en ligne de commande, on peut utiliser les commandes mvn clean generate-sources et mvn clean install.
  • Analyser rapidement les traces de l'exécution (génération, compilation, exécution des tests JUnit) et les codes produits (fichiers *.mips.

Comme le code du compilateur MiniJAVA est organisé en paquetage selon les phases d'exécution du compilateur, de l'analyse lexicale à l'exécution du code assembleur MIPS dans le simulateur MARS, voici représenté graphiquement les phases du compilateur MiniJAVA :


Phases du compilateur MiniJAVA

Les questions et la curiosité sont bienvenues.

Analyse lexicale et syntaxique

Préliminaire


Le compilateur fourni est fonctionnel mais uniquement pour une syntaxe très minimaliste (Hello 42 dans le fichiel ./src/test/resources/Milestones/Test101.txt, qui correspond au Jalon 1). Dès que l'on va étendre la syntaxe, le comportement du compilateur est indéfini.

Les différentes phases du compilateur sont exécutées en séquence dans la méthode main.Compiler::main : les analyses lexicale et syntaxique ensemble, puis l'analyse sémantique, ensuite la génération de la représentation intermédiaire, et enfin la génération de code avec l'exécution du code MIPS généré par l'outils MARS.

À la fin de chaque phase, l'instruction en commentaire Compiler.stopAfterXxxx() permet d'arrêter la séquence. Par exemple, l'instruction Compiler.stopAfterSyntax() est utilisée lorsque l'on souhaite compléter et tester la spécification JFlex+CUP sans que les autres phases n'aient été écrites. (Ces mêmes instructions sont utilisées dans certains tests JUnit.)

Comme pour les différentes phases du compilateur, le travail est à réaliser de manière incrémentale en ajoutant au fur et à mesure les différentes règles de la grammaire (Jalons 1,..., 9) et en validant avec les tests JUnit afférents ou avec un fichier input.txt à compléter ou adapter.

L'ensemble du compilateur est réalisé dans un premier temps en ignorant les tableaux d'entiers (Jalon 9). L'étape finale du projet ajoute les tableaux d'entiers dans l'ensemble des phases du compilateur.

Le langage MiniJAVA est un sous-ensemble du langage Java. Pour les tests, il peut donc être utile dans les différentes étapes du projet de comparer le comportement de votre compilateur MiniJAVA avec celui de javac.

Analyse lexicale et syntaxique


Réaliser l'analyse lexicale et syntaxique de MiniJAVA pour les Jalons&nbps;2 à 7.

  • Lire la documentation du projet Mémento MiniJAVA, sections 2, 3, 4 et 10.
  • Écrire les spécifications JFlex et CUP pour réaliser l'analyse du langage MiniJAVA :
    • L'identificateur this peut être ignoré en temps que mot-clé dans l'analyse lexicale. Il est traité comme les autres noms de variable dans l'analyse syntaxique. Le rôle particulier de this sera pris en compte au niveau de l'analyse sémantique qui forgera la déclaration implicite de cette variable.
    • La gestion des localisations dans le fichier source (méthode AddPosition) est optionnelle, mais conseillée car elle aide lors du déverminage.
  • Construire l'Arbre de Syntaxe Abstraite en suivant les définitions du paquetage phase.b_syntax.ast.*.
  • Tester et valider l'analyse syntaxique en vérifiant l'impression des AST, et le résultat du visiteur phase.b_syntax.PrettyPrint. Adapter si besoin la classe main.util.Debug pour modifier les traces imprimées par le compilateur.
  • Utiliser les classes de tests dans ./src/test/java/test pour les tests. En particulier :
    • pour tester les analyses avec des entrées correctes, utiliser la classe SuccessfulMilestonesTest avec :
      • soit les méthodes jalonString[1-7], qui utilisent en entrée des chaînes de caractères définies dans la classe de test,
      • soit les méthodes jalonFile[1-7], qui utilisent en entrée les fichiers dans le répertoire src/test/resources/Milestones/.
      Nous proposons les deux formes (chaîne de caractères et fichiers) pour, lors du déverminage, laisser choisir entre « jouer » avec des chaînes de caractères ou avec les contenus des fichiers ;
    • pour tester les analyses avec des entrées incorrectes, et qui doivent être détectées comme étant incorrectes, utiliser la classe ErrorsLexicalSyntacticTest.
    • pour tester des exemples plus conséquents, utiliser la classe RunningTest avec les fichiers dans le répertoire ./src/test/resources/Running/ : les fichiers Test20[1-7].txt, AckermannFunction.txt, ImplemSimplisticBigNum.txt, etc.

Voici par la construction par étape de la spécification JFlex+CUP les jalons :

  • Jalon 2 : grammaire d'opérateur et priorités, cf. les exercices JFlex+CUP sur « Calculatrice »,
  • Jalon 3 : listes simples, cf. les exercices JFlex+CUP sur les « Grammaires de listes »,
  • Jalon 4 : listes avec séparateurs, cf. les exercices JFlex+CUP sur les « Grammaires de Listes »,
  • Jalon 5 : doubles listes, cf. les classes KlassBody et MethBody dans la spécification CUP fournie,
  • Jalon 6 : autres expressions,
  • Jalon 7 : autres instructions.

Les classes du paquetage phase.b_syntax.ast sont celles qui peuvent être utilisées dans le code JAVA des actions des règles de production de la spécification CUP. A priori, et sauf erreur de notre part, la liste des classes est complète.

Analyse Sémantique : Table des symboles

Préliminaire


Cette étape suppose d'avoir une analyse lexicale et syntaxique opérationnelle pour le langage MiniJAVA. Compléter l'étape précédente en utilisant éventuellement le corrigé.

On n'oubliera pas de lire le Mémento MiniJAVA qui contient un ensemble d'informations utiles ou indispensables qui ne sont pas reprises dans cette page : Sections 4 à 7 pour cette étape.

Petite Visite


Écrire un visiteur phase.c_semantic.DisplayVarDeclarations qui hérite de syntax.ast.AstVisitorDefault et qui imprime l'ensemble des identificateurs des variables déclarées.
Le visiteur phase.b_syntax.PrettyPrint peut donner une idée de squelette, mais le travail demandé ici est beaucoup plus court (20-30 lignes). DisplayVarDeclarations commence comme suit :

public class DisplayVarDeclarations extends AstVisitorDefault { private final IndentWriter out = new IndentWriter(); public DisplayVarDeclaration(final SemanticTree semTree) { out.print("= Test des Identificateurs : "); semTree.axiom().accept(this); Debug.log(out); } ...

Inclure l'appel au visiteur dans phase.c_semantic.Semantic pour tester : pour ce premier visiteur, la construction d'une instance de la classe suffit (l'appel à la méthode accept sur l'objet axiome démarre la visite et la méthode Debug::log affiche le log accumulé). Voici le début complété de la séquence de visiteurs appelés dans la méthode Semantic::run :

public SemanticTree run() { // Premières visites pour s'entraîner new DisplayVarDeclarations(this.semanticTree); ... }

Adapter le visiteur pour préciser, pour chaque variable, s'il s'agit d'un attribut de classe, d'une variable locale, ou d'un paramètre formel de méthode.
Voici un exemple de résultat d'exécution sur le fichier exemple sur ./src/test/resources/Milestones/Test105.txt :

= Affichage des Identificateurs : Test2 (klasse), a(field), b(field), Start(method), i(formal), j(formal), k(local), un(method), Test3 (klasse), zero(method),

Compléter le visiteur pour afficher les noms de classes et de méthodes déclarées.

Au vu de l'énonce et de l'affichage, les méthodes visit sont à redéfinir pour les nœuds de type Formal, Klass, Method, et Variable.

Contre-Visite


Écrire un autre petit visiteur phase.c_semantic.DisplayScopes qui reconstruit uniquement la structure des accolades {} du fichier d'origine.

Voici un exemple de résultat d'exécution sur le fichier exemple sur ./src/test/resources/Milestones/Test105.txt :

= Affichage des portées : Test2{Start{}un{}}Test3{zero{}}

Fusion


Fusionner les 2 visiteurs précédents dans un seul visiteur DisplayVarDeclarationsInScopes. On imprime ainsi l'ensemble des identificateurs à l'intérieur de leur portée respective.

Voici un exemple de résultat d'exécution sur le fichier exemple sur ./src/test/resources/Milestones/Test105.txt : = Affichage des identificateurs dans leur portée : Test2{a(field), b(field), Start{i(formal), j(formal), k(local), }un{}}Test3{zero{}}

Bravo, vous savez parcourir l'AST pour afficher l'arbre des portées (scope) des identificateurs et pour aussi trouver l'ensemble des déclarations. En somme, une table des symboles presque complète ! Il y manque la construction des structures de données de la table des symboles.

Table des Symboles


Outre le Mémento MiniJAVA, Il convient de lire attentivement le contenu des classes du paquetage phase.c_semantic.symtab.

La construction de la Table des symboles pour le compilateur MiniJAVA est réalisée par la classe phase.c_semantic.BuildSymTab. Le squelette fourni est un visiteur qui ne fait rien, mais contient les helpers utiles pour réaliser le travail. Analyser les helpers avant de commencer le travail ; souvent ils répondent à la question « où est-ce que cela se trouve ? »

On notera que :

  • la table des symboles (rootScope) est un attribut de l'enregistrement phase.c_semantic.SemanticTree. C'est un objet de type Scope ayant pour nom « Root » et n'ayant pas de portée parente ;
  • à la lecture de l'enregistrement SemanticTree, on voit que la table des symboles est un des attributs sémantiques de l'analyse sémantique du projet MiniJAVA. Sans surprise, on trouvera que l'attribut qui sera étudié à l'étape suivant celle-ci est le typage des nœuds de l'AST, principalement pour le contrôle du type, le langage JAVA étant un langage dit fortement typé ;
  • la classe Object et sa méthode equals sont déjà intégrées dans la table des symboles avec le code fourni : la méthode addObjectKlass est appelée au début de la méthode SemanticTree::run et crée un objet de type InfoMethod pour la méthode Object::equals avant de l'ajouter dans la portée de la classe Object en appelant la méthode newMethodScope ;
  • tout programme MiniJAVA commence conventionnellement par la classe Main : cf. la section 2.1 du memento MiniJAVA. Aussi, la classe Main de MiniJAVA est intégrée dans le code du squelette à titre d'exemple de méthode BuildSymTab::visit. Les symboles de cette classe (main(), args, className) n'ont en réalité pas d'utilisation dans le langage MiniJAVA.

Écrire la classe BuildSymTab qui construit la table des symboles pour le langage MiniJAVA :

  • Construire l'arbre des portées qui structure la table des symboles : (cf. Mémento MiniJAVA, sources, et helpers).
  • Calculer l'attribut sémantique hérité currentScope qui associe à chaque nœud de l'AST la portée où se trouve le nœud. Cet attribut réalise le lien vers les identificateurs visibles depuis ce nœud.
  • Stocker l'attribut sémantique currentScope (décoration de l'AST) dans la structure semantic.semanticTree (attribut scopeAttr) pour pouvoir le réutiliser dans la suite de l'analyse sémantique et dans les phases suivantes du compilateur (cf. méthode helper) setScope.
  • Stocker dans l'arbre des portées l'ensemble des informations de déclaration d'identificateurs (classe, méthode, variable/attribut) en utilisant les méthodes phase.c_semantic.symtab.Info*.
  • Gérer la « variable » this dont la déclaration est implicite dans l'AST :
    • Pour chaque méthode, ajouter dans la table des symboles une déclaration explicite de this comme premier paramètre de la méthode.
    • Le type de la variable this est le nom de la classe courante !
    • Il faut dont calculer l'attribut sémantique hérité currentKlass qui associe à chaque nœud de l'AST, la classe englobante du nœud. Il n'est pas utile de stoker cet attribut dans l'arbre sémantique car l'attribut BuildSymTab::currentKlass correctement mis à jour lors de la visite suffit.
  • Réaliser enfin la fonction sémantique allready defined qui détecte les erreurs de redéfinition d'identificateur au sein d'une même portée. Ce n'est fonctionnellement pas l'endroit pour faire cela, mais c'est très économique de le faire ici (et c'est déjà presqu'écrit avec le code de la méthode BuildSymTab::checkRedef !).
  • Valider le travail en vérifiant l'impression de la table des symboles (passe 1 de la phase 3, qui est l'analyse sémantique).

Comme préparé dans les premiers visiteurs avec la fusion dans la classe DisplayVarDeclarationsInScopes, la table des symboles est d'abord remplies pour la déclaration des portées et des variables. Donc, les visites des nœuds de l'AST de type Klass, Method, Variable, et StmtBlock doivent être redéfinies dans le visiteur BuildSymTab. Ensuite, on n'oubliera pas de gérer currentScope et currentKlass comme indiqué ci-dessus.

Voici un exemple de résultat d'exécution sur le fichier exemple sur ./src/test/resources/Milestones/Test105.txt :

= Table des symboles (passe1) Scope Root class Object extends null class Test105 extends Object class Test3 extends Test2 class Test2 extends Object Scope Object boolean equals(Object this, Object o) Scope equals_args Object this Object o Scope equals Scope Test105 void main(String[] args) Scope main_args String[] args Scope main Scope Test2 int Start(Test2 this, int i, int j) int un(Test2 this) int a int b Scope Start_args Test2 this int i int j Scope Start int k Scope un_args Test2 this Scope un Scope Test3 int zero(Test3 this) Scope zero_args Test3 this Scope zero

Table des symboles et Héritage Orienté Objet

Avant d'utiliser l'héritage des classes JAVA, il est urgent de valider l'absence de boucles dans l'héritage. Cette fonction sémantique est réalisée par la classe semantic.CheckInheritance (algorithmique très basique et peu optimisé !).

Dans la même classe, on va de plus reconstruire la racine de l'arbre des portées pour ajouter l'arbre d'héritage des classes (si c'est un arbre !) comme arbre d'héritage des visibilités des identificateurs. L'héritage orienté objet deviendra alors transparent pour la recherche dans la table des symboles.

Parcourir rapidement le code et tester le fonctionnement en analysant l'impression de la table des symboles (passe 2).

La gestion des erreurs est ici optimiste, c'est-à-dire que l'erreur a priori grave est signalée, mais on continuera la phase d'analyse sémantique simplement en ignorant l'héritage JAVA pour les classes qui n'ont pas Object comme ancêtre.

Allready defined


Écrire la fonction sémantique Identifier allready defined.

Déjà fait en même temps que la construction de la table des symboles en utilisant la méthode BuildSymTab::checkRedef !

Comparer le comportement du compilateur MiniJAVA avec celui de javac dans le cas d'une variable locale de méthode qui redéfinit un argument de la méthode.

javac considère comme une erreur de redéfinition, l'écrasement d'un paramètre d'appel par une variable locale. Notre compilateur l'accepte.

(Bonus 1.1) Undefined


Écrire un nouveau visiteur qui vérifie que tous les identificateurs utilisés dans l'AST, sont bien liés à une déclaration dans la table des symboles.

Ignorer pour le moment les identificateurs de méthode, qui ne peuvent être traités qu'après le contrôle de type : rechercher la méthode pour obj.method() nécessite de connaître le type de obj. N.B. : si l'on prend en compte la liaison dynamique de JAVA, le test n'est plus après le contrôle de type mais éventuellement à l'exécution !

On peut aussi ignorer l'identificateur de classe de la classe parente dont le contrôle est déjà réalisé de fait dans semantic.CheckInheritance.

À venir après le contrôle de type !

(Bonus 1.2) Unused


Intégrer dans le visiteur Undefined précédent la fonction sémantique qui signale l'ensemble des variables déclarées mais non utilisées. On notera que semanticTree.rootScope.getAllVariables() donne la collection de toutes les variables du programme. Il suffit donc de supprimer au fur et à mesure les variables utilisées pour avoir en fin de visite les variables Unused.

À venir après le contrôle de type !

Gestion d'erreurs


cf. Mémento MiniJAVA et code fourni...

Analyse Sémantique : Contrôle de type

hors bonus, cette étape est sans conteste la plus consommatrice de temps et la plus intéressante.

Question de Cours Orienté Objet


Le schéma de liaison entre les identificateurs et leurs déclarations réalisé avec la table des symboles consiste à rechercher la déclaration en partant de la portée courante et en remontant l'arbre des portées (cf. Scope::lookup*()).

Ce schéma fonctionne pour les variables ou pour les méthodes de classe (static) mais ne fonctionne pas directement pour les méthodes d'instance. Pourquoi ?
Si l'on a x.get(); y.get(); dans une même portée, où chercher la déclaration de get() ?
En fait la question est incorrecte. Il y a potentiellement plusieurs déclarations différentes.
solution ?...

Type top


Écrire un visiteur qui calcule l'attribut Type de chaque expression dans l'AST MiniJAVA. Le Type est un String avec des valeurs prédéfinies (main.EnumType) pour les types primitifs, ou un nom de classe pour les types Objets. La classe phase.c_semantic.TypeChecking est fournie comme squelette pour démarrer la construction du contrôle de type.

Le Type est un attribut synthétisé qui est stocké dans la structure SemanticTree pour la suite de la compilation. L'attribut Type est nécessaire pour le contrôle de type à suivre, et sera aussi indispensable pour l'allocation mémoire dans la phase de génération de code assembleur.

Spoiler : en réalité, on ignorera l'attribut en allouant un mot de quatre octets pour toutes les variables quelque soit le type !

La réalisation de cette question peut se faire en parallèle de la question suivante. Le test et la validation sont en particulier communs aux 2 questions.

Top type


Intégrer dans le visiteur précédent le contrôle de type, c'est-à-dire la vérification de l'ensemble des contraintes de typage du langage MiniJAVA.

Dans MiniJAVA, il n'y a pas de transtypages pour les types primitifs. Pour les types Object, on a le transtypage naturel lié à l'héritage des classes. Un objet possède le type de tous ces ancêtres. Ce mécanisme est implanté dans le squelette fourni et l'on a aucun besoin de mémoriser ces cas de transtypage pour les phases suivantes de la compilation.

On notera l'absence de surcharge des opérateurs, c'est-à-dire la possibilité qu'un opérateur change de sémantique en fonction du type des opérandes. Pour les méthodes, on ignore la liaison dynamique et les possibilités de surcharge, mais on permet la redéfinition (overriding).

Étudier les nombreuses méthodes helpers :

  • erreur : permet d'afficher un message d'erreur en gardant mémoire qu'il y a eu une erreur, mais sans arrêter l'analyse. L'intérêt de ne pas s'arrêter à la première erreur est d'essayer d'en détecter plusieurs en une seule exécution du compilateur MiniJAVA ;
  • getType : par exemple, lors de la visite d'un nœud, permet d'obtenir le type d'un nœud enfant (il faut dans ce cas avoir visiter cet enfant avant l'appel) ;
  • setType : par exemple, sur un nœud opérande litéral d'une expression, permet de fixer le type. Conseil : utiliser le type VOID lors d'une erreur, par exemple avec un opérateur binaire inconnu dans une expression ; cela permet de continuer l'analyse du typage ;
  • getScope et lookupKlass : par exemple, getScope cherche la méthode dans la portée d'une classe (trouvée avec lookupKlass) lors d'un appel de méthode (nœud de type ExprCall) ;
  • compareType et checkType : on fournit ces deux méthodes, la seconde appelant la première pour accélérer le développement. Leur lecture est importante pour la compréhension de la gestion du typage pour l'héritage dans le compilateur MiniJAVA. La méthode checkType est la méthode par excellence qui est appelée lors de la visite d'un nœud ;
  • checkTypeName : permet, par exemple, lors de la visite d'un nœud de déclaration d'un nouveau type (ExprNew) ou d'utilisation d'un type (Type), de vérifier que le type est connu ;
  • lookupVarType : après la recherche de la variable dans la portée d'un nœud de l'AST, cherche le type de la variable. Cette méthode est utile lors de la visite des nœuds utilisant des identifiants, et plus précisément ExprIdent et StmtAssign. On notera que le type VOID est fourni lorsque la variable n'a pas été trouvée ;

Tester au fur et au mesure sur des exemples valides (p.ex. dans la classe de tests SuccessfulMilestonesTest) ou invalides (p.ex. dans la classe de tests ErrorsSemanticsTypingTest). En cas de doute, on peut aussi comparer avec le comportement du compilateur javac sur les mêmes exemples.

Bonus 1 (redite)


Terminer et valider les questions « Undefined » et « Unused » de l'étape 2 en intégrant la recherche des méthodes. On ne traite que la liaison statique et l'on ignorera le polymorphisme des méthodes.

Génération de la Forme Intermédiaire

Hypothèses pour la génération de code


Pour simplifier les phases suivantes de la compilation, on réduit en peu le spectre du compilateur en supposant que :
  • Les méthodes deviennent maintenant globales, c'est à dire que les noms de méthodes sont uniques dans le fichier source. Dans la représentation intermédiaire comme dans le code final MIPS, une méthode correspond à un label avec le nom de la méthode. En cas de duplication, la détection se fera au niveau de l'assemblage avec MARS et un message duplicated label.
  • Toutes les "Variables" de MiniJAVA ou de la forme intermédiaire auront une implantation en mémoire identique avec un mot de 32 bits. On simplifie ainsi le calcul des tailles, des alignements...

Traduction AST vers IR


Écrire un traducteur de l'AST vers la Représentation Intermédiaire "code à 3 adresses" (cf. Mémento MiniJAVA section 8). La classe intermediate.Intermediate donne un canevas de départ avec les helpers utiles.

Il s'agit d'écrire un visiteur de l'AST avec :
  • Calcul (et stockage) d'un Attribut synthétisé Var, qui pour chaque nœud "expression" contient la variable (IRvariable) utilisée par la forme intermédiaire pour stocker le résultat de l'expression. Cette variable est généralement une variable temporaire générée par le traducteur mais peut aussi être une constante ou une variable du programme d'origine.
  • Calcul d'un attribut hérité currentMethod qui contient le nom de la méthode courante. Cette information est utilisée uniquement pour associer une portée locale aux variables temporaires IRtempo et permettre une meilleure allocation mémoire à venir.
  • Traduction des nœuds de l'AST sous forme de séquences d'instructions IRquadruple. Les exemples de base pour les schémas de traduction sont illustrés dans les diapositives du cours.
  • La traduction se fait à la volée au sens où le programme final est la concaténation des traductions de chaque nœud de l'AST en suivant l'ordre de la visite.
Réaliser le travail par étape en reprenant les milestones de la syntaxe MiniJAVA et les exemples ./Exemples/Milestone/Test10*.java.
Exemple de résultat :

(Bonus) Propagation de constantes


Une optimisation dans la génération de la forme intermédiaire consiste à évaluer directement les opérations sur des constantes plutôt que de traduire de manière générique pour faire cette évaluation à l'exécution. Modifier le schéma de traduction pour le nœud ExprOpBin sous la forme
/* exp1 op exp2 */ traduction(exp1) traduction(exp2) si (getVar(exp1) instanceof IRconst) et (getVar(exp2) instanceof IRconst){ setVar Node, newConst("getValue"(exp1) op "getValue"(exp2)) } sinon { setVar node, newTemp() QAssign op, getVar(exp1), getVar(exp2), getVar(node) }
Idem pour ExprOpUn.

(Super Bonus) Optimisation d'expressions booléennes


Pour optimiser l'exécution, mais aussi parce que c'est la sémantique dans le langage java, modifier le schéma de traduction pour l'opérateur booléen "ET" (&&) de manière à ne pas évaluer les 2 opérandes quand cela n'est pas nécessaire. De manière plus générale, la traduction sur les expressions booléennes se fera en utilisant plutôt une forme d'instruction "IF" qu'une forme d'opérateur logique.

N.B. : En java, les expressions (n != null) && (n.istrue) ou (n == null) || (n.istrue) sont correctes (pas de NullPointerException) car le deuxième opérande n'est évalué que en fonction de la véracité du premier opérande.
Traductions des opérateurs logiques en opérateur ternaire : exp1 && exp2 == exp1 ? exp2 : exp1 exp1 || exp2 == exp1 ? exp1 : exp2 ! exp1 == exp1 ? false : true

Le schéma de traduction de && devient :
/* exp1 && exp2 */ traduction(exp1) setVar node newTemp() QCopy getVar(exp1), getVar(node) QJumpCond L0, getVar(exp1) traduction(exp2) QCopy getVar(exp2), getVar(node) QLabel L0

Génération de l'assembleur MIPS

Prise en main


La génération de l'assembleur est réalisée par la classe codegen.CodeGen qui utilisera les classes des packages codegen et codegen.access :
  • La classe Reg fournit une énumération des registres MIPS pour les autres classes du package.
  • La classe MipsWriter est une classe utilitaire pour l'impression dans un fichier des instructions MIPS. On peut l'adapter ou la compléter à la convenance de chacun.
  • La classe LinkRuntime réalise l'édition des liens qui se limite à la concaténation du Runtime MIPS.
  • La classe ToMips réalise la traduction de la forme intermédiaire vers MIPS. Pour le moment, elle ne gère que le programme hello 42 (Test101).
  • La classe Allocator et le package codegen.access construit, à partir de la table des symboles (AST + IR), l'allocation mémoire des variables, des classes, des cadres d'appel... (cf. question suivante).
Analyser rapidement le contenu et le fonctionnement de ces classes.

Pour terminer notre compilateur, seule la classe ToMips nécessite d'être modifiée.

Allocation mémoire


Analyser en détails le contenu de la classe codegen.Allocator. Expliquer en particulier :
  • Les différents types d'accès aux variables de la forme intermédiaire et la mise en œuvre à travers les classes codegen.access.Access*.
  • La gestion des variables instances de classe : instanciation et accès aux champs.
  • La mise en œuvre de la convention d'appel et le calcul des tailles des cadres d'appel (frame).
  • L'allocation des variables locales, temporaires, ou globales.

Traduction : sélection d'instruction et convention d'appel


Écrire par étapes (milestones) la classe ToMips pour réaliser la compilation du langage MiniJAVA. Enfin le bout du tunnel !!!
  • Analyser la structure de la classe ToMips : "Visiteur IR", Helpers, gestion des appels "spéciaux" (_system_*),...
    • L'accès aux variables IR (utilisation de codegen.access) est réalisé avec les méthodes regLoad(), regStore().
    • Les registres $v0 et $v1 sont librement accessibles pour les calculs temporaires dans le traitement de chaque instruction IR.
    • La gestion des paramètres est réalisée avec une liste java.util.ArrayList<IRvariable> params, qui est remplie par les instructions QParam, et utilisée puis réinitialisée dans les instructions QCall et QCallStatic.
    • ...
  • Étapes :
    • Traduction de l'instruction QAssign(+,*).
      • Charger les 2 arguments de QAssign dans $v0 et $v1.
      • Effectuer l'opération avec résultat dans $v0.
      • Enregistrer $v0 dans le résultat de QAssign.
    • Traduction de l'instruction QNew.
      • Sauvegarder le registre $a0
      • Charger dans $a0 la taille de l'objet à instancier (cf. Allocator.classSize())
      • Appeler la fonction _new_object du Runtime MIPS
      • Enregistrer $v0 (résultat de _new_object) dans le résultat de QNew.
      • Restaurer $a0.
      • N.B. : On applique ici une convention d'appel simplifiée.
    • Construction la convention d'appel : QLabelMethod et QReturn pour l'appelé, QCall et QParam pour l'appelant.
      • QLabelMethod :
        • Écrire le label de fonction.
        • Positionner le cadre d'appel (framepointer) $fp = $sp.
        • Allouer le cadre d'appel = avancer la pile de la taille du cadre d'appel Allocator.frameSize().
        • Sauvegarder les registres adéquats ($ra,$s0,$s1,...).
        • Recopier les valeurs des arguments $a1, $a2, $a3 dans les 3 premiers variables locales du cadre d'appel.
      • QReturn :
        • Restaurer les registres adéquats.
        • Charger dans $v0 la valeur de retour.
        • Libérer le cadre d'appel. On notera que $fp contient la valeur de $sp à restaurer !
      • QCall :
        • Sauvegarder les registres adéquats ($fp,$a0,$a1,$a2,$a3,$t0,$t1,...).
        • Charger les (maximum 4) paramètres dans les registres $a0,$a1,$a2,$a3. Attention : pour éviter un éventuel problème d'écrasement, le registre $a0 doit être chargé (et écrasé) en dernier.
        • Effectuer l'appel : jal.
        • Restaurer les registres adéquats.
        • Enregistrer $v0 comme résultat du QCall.
    • et le reste : QAssign*, QCopy, QLabel, QJump*,...
      Aide-toy, le Ciel t’aidera. — (Jean de la Fontaine, Le Chartier embourbé, dans Fables, livre sixième, 1692-94)
Exemple de résultat en ignorant les boni de la forme intermédiaire : .

Validation


Valider le fonctionnement du compilateur sur les exemples du dossier ./Exemples/* et avec un fichier input.txt à construire à cet effet.

Pour le test sur différents fichiers, il peut être utile de travailler en ligne de commande :
  • la cible jar de make ou de ant permet de créer une archive Compiler.jar exécutable du compilateur.
  • L'exécution du compilateur se fait alors avec : java -jar Compiler.jar fichier1 fichier2....
  • On teste ensuite chaque fichier assembleur produit avec mars fichier.mips.

(Bonus) Manque d'arguments


Supprimer la contrainte sur le nombre d'arguments des méthodes. La classe codegen.Allocator implémente déjà la convention d'appel, et définit l'accès aux arguments 4 à N (0($fp),4($fp)..., c'est à dire dans la pile avant la frame de l'appelé). Il faut uniquement modifier les séquences d'appel et de sortie au niveau de l'appelant.
  • Modifier le traitement de l'instruction QCall pour accepter un nombre quelconque d'arguments dans le langage MiniJAVA.
  • Attention de bien empiler (modif de $sp) les arguments supplémentaires avant de créer la frame ; et de dépiler à la fin.
  • Vérifier que le compilateur marche encore pour une fonction récursive comme factorielle ou Ackermann.
  • Vérifier aussi le fonctionnement sur ./Exemples/Running/Test205.java.

On passe aux tableaux

Les étapes précédentes fournissent un compilateur complet mais qui ne traite pas l'ensemble du langage MiniJAVA. Il s'agit donc dans cette dernière étape, d'intégrer dans l'ensemble des phases du compilateur la gestion des tableaux d'entiers du langage MiniJAVA. L'énoncé qui suit donne quelques indications mais l'énoncé réel de cette étape est dans toutes les étapes qui précédent et dans le Mémento MiniJAVA.

Préambule


Quelques suggestions avant de démarrer :
  • Vérifier que le compilateur de départ est raisonnablement correct (cf. dernière question de l'étape 5).
  • Faire un commit/backup/mail_a_1_ami pour ne pas casser le "compilateur qui marche".
  • Prévoir éventuellement des tests simples de non-régression (un petit script qui compile N fichiers de tests, et imprime le résultat de l'exécution avec mars).

Analyse lexicale et syntaxique


Un token et cinq règles de grammaire à ajouter dans les spécifications JFlex et CUP.
Quatre nouveaux nœuds à utiliser dans l'AST : syntax.ast.*Array*.
Ne pas oublier de fixer une priorité pour les tokens []. (Bonus : tester aussi le comportement sans cette priorité)

Analyse Sémantique


La table des symboles est inchangée à part l'existence d'un type main.EnumType.INT_ARRAY.
Le contrôle de type doit traiter les 4 nouveaux nœuds de l'AST.

Représentation Intermédiaire


Les 4 nouveaux nœuds de l'AST correspondent à 4 nouvelles instructions de la Représentation Intermédiaire.
Exemple de résultat :

Génération de code MIPS


Traduire les 4 nouvelles instructions IR dans ToMips*.

L'instanciation des tableaux utilisera la fonction _new_object du Runtime MIPS, avec le schéma suivant à implémenter :
_new_object avec IN $a0 = 4 * LENGTH + 4, OUT $v0 LENGTH == taille du tableau, à enregistrer à l'offset 0 du tableau [-- 4 --] [-------------- 4 * LENGTH ---------------------] +---------+--------+--...---+--------+--...---+-------------+ | LENGTH | [0] | ... | [i] | ... | [LENGTH-1] | +---------+--------+--...---+--------+--...---+-------------+ ^ ^ $v0 $v0 + 4 + 4 * i

Une variable tableau proprement dite est gérée de façon transparente par Allocator pour qui toutes les variables sont des valeurs sur 4 octets, aussi bien pour les types primitifs (entier, booléen), que pour les types références (instance de classe ou tableau).
Exemple de résultat :

Validation


Valider le bon fonctionnement du compilateur. Utiliser les Exemples */Test*9.java et le très bel exemple TestBigNum qui calcul 42 comme somme de 3 cubes ! Enfin la vraie révélation du secret de l'univers !!! (ou du metavers !)

Quand on dépasse les bornes, il n'y a plus de limites ! (Bonus)


Ajouter le contrôle de bornes à l'exécution sur les tableaux. Ajouter aussi le contrôle à l'exécution sur la taille positive dans l'instanciation des tableaux
  • Utiliser l'instruction MIPS : sltu $t0, $t1, $t2 (?!?).
  • Gérer une sortie en erreur en utilisant la fonction _system_exit(errorCode) du Runtime MIPS (errorCode=666 ou 42 ou...).

Bonus rapide : Tableaux de Booléens


Ajouter au langage MiniJAVA, les tableaux de booléens.
  • Ajouter un nom de type dans main.EnumType et une règle dans la spécification CUP.
  • Adapter éventuellement le nœud de l'AST ExpArrayNew pour le contrôle de type.
  • Compléter le contrôle de type sur les tableaux (entier ou booléen).
  • Et cela marche, ou pas !!!

Rendu final

Le travail à rendre est une archive .tar.gz (ou autre) du compilateur complet. En plus des fonctionnalités dont le corrigé est fourni, le compilateur intègre :
  • La gestion des tableaux d'entiers.
  • Un fichier de test input.txt qui couvre les fonctionnalités du compilateur et qui est valide pour le compilateur.
  • Un fichier Readme avec le nom des auteurs, et les commentaires utiles pour le correcteur : on a fait comme il fallait mais ça ne marche pas, on n'a pas fait comme il fallait mais ça marche mieux !, on a mis plein de bonus (ou boni),...
  • (Bonus&nbps;1) Étape 2-3 : La fonction sémantique Undefined/Unused pour les variables, les classes et les méthodes.
  • (Bonus) Étape 4 : Propagation de constantes.
  • (Bonus) Étape 4 : Optimisation d'expression booléenne.
  • (Bonus) Étape 5 : Manque d'arguments.
  • (Bonus) Étape 6 : Contrôle de borne de tableau.
  • (Bonus) Étape 6 : Tableaux de booléens.
  • (Bonus) Tableau d'objets, null, == (opérateur surchargé), division euclidienne (/,%), System.exit(i),....

Consignes :
  • Faire le ménage (make clean, ou ant clean) avant de construire l'archive.
  • Exclure le dossier lib de l'archive si ce n'est pas un lien symbolique.
Barème indicatif :
  • 4 points : Sources d'origine et codes des étapes 1 à 5 (corrigés fournis)
  • 3 points : Readme et input.txt
  • 9 Points : Tableaux d'entiers
  • 1 à 2 points par bonus
  • Total : 16 sans bonus, 20 avec 3 boni, 25 avec les 7 bonus,...

CSC4251_4252, Télécom SudParis, Pascal Hennequin, Denis Conan, J. Paul Gibson Dernière modification : Janvier 2025