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

Portail informatique

Mémento JAVA

Ce mémento résume les concepts et idiomes JAVA ainsi que les patrons de conception utilisés dans le module.

Patron de conception Visiteur

Pour présenter le patron de conception Visiteur, on prend l'exemple de l'exercice CUP intitulé « Calculatrice et première visite ».

Le diagramme de classe qui suit présente la plupart des classes JAVA utilisées dans la spécification CUP (nommément, Exp, ExpAff, ExpEntier, ExpFmax, ExpOpBin, ExpVar). Pour rappel, dans un diagramme de classes, il n'est pas obligatoire de présenter toutes les classes, tous les attributs, et toutes les opérations.


Diagramme de classes pour l'exercice CUP intitulé « Calculatrice et première visite »

Avec la chaîne de caractères « max((2*21+1)/4, a=max(42, 2), b=c=4*a/42)\n », l'analyseur syntaxique exécutera la règle de production suivante :

ligne ::= expr:e {: new VisiteurCompteurOpBin(e); System.out.println("AST Eval=" + e.eval()); System.out.println(e.toPrint()); :}
Autrement dit, le caractère « \n » à la fin de la chaîne de caractères provoque la réduction de la règle de production pour une ligne (complète), et dans le code de l'action associée, un objet visiteur VisiteurCompteurOpBin est créé, qui évalue l'expression, c'est-à-dire qui exécute une visite de l'AST de l'expression. Ensuite, le nœud e est évalué avec l'instruction e.eval() pour calculer la valeur ou résultat de l'expression. Enfin, l'AST du nœud e est affiché. Cela donne l'affichage qui suit :
Reading standard input Calculatrice avec évaluation sur AST (et location) > Nombre d'opérateurs binaire = 5 AST Eval=42 ExpFmax[1/1(1)-1/41(41)] \-AstList[1/5(5)-1/40(40)] |-ExpOpBin[1/5(5)-1/15(15)](/) | |-ExpOpBin[1/5(5)-1/13(13)](+) | | |-ExpOpBin[1/6(6)-1/10(10)](*) | | | |-ExpEntier[1/6(6)-1/7(7)](2) | | | \-ExpEntier[1/8(8)-1/10(10)](21) | | \-ExpEntier[1/11(11)-1/12(12)](1) | \-ExpEntier[1/14(14)-1/15(15)](4) |-ExpAff[1/16(16)-1/28(28)] | |-ExpVar(a) | \-ExpFmax[1/18(18)-1/28(28)] | \-AstList[1/22(22)-1/27(27)] | |-ExpEntier[1/22(22)-1/24(24)](42) | \-ExpEntier[1/26(26)-1/27(27)](2) \-ExpAff[1/30(30)-1/40(40)] |-ExpVar(b) \-ExpAff[1/32(32)-1/40(40)] |-ExpVar(c) \-ExpOpBin[1/34(34)-1/40(40)](/) |-ExpOpBin[1/34(34)-1/37(37)](*) | |-ExpEntier[1/34(34)-1/35(35)](4) | \-ExpVar[1/36(36)-1/37(37)](a) \-ExpEntier[1/38(38)-1/40(40)](42)

Le diagramme de séquence qui suit présente le début de la visite de l'AST de l'expression « max((2*21+1)/4, a=max(42, 2), b=c=4*a/42)\n » avec le passage par les nœuds de ExpMax, AstList, ExpOpBin conformément à l'AST affiché ci-avant. En guise d'exercice de compréhension du patron de conception Visiteur, on lira en parallèle (i) le diagramme, (ii) l'affichage de l'AST, et (iii) le code JAVA de la solution à l'exercice CUP : l'objectif est de retrouver la première incrémentation « nbOp++ ».


Diagramme de séquence du début de la visite de l'AST de l'expression « max((2*21+1)/4, a=max(42, 2), b=c=4*a/42)\n »

Records et patron de conception Fabrique (Factory)

Records


Lors de l'analyse syntaxique, l'arbre de syntaxe abstrait est consruit avec des nœuds qui sont des instances de classes réalisant l'interface AstNode. Par ailleurs, une fois construit, ces nœuds n'ont pas vocation à être modifiés : ils sont dits « immuables », c'est-à-dire leurs attributs sont mis final en JAVA.

On se plaint souvent que JAVA est trop « verbeux ». Cet inconvénient est particulièrement visible dans le contexte de la construction d'arbres avec des nœuds immuables. Ces classes qui modélisent des tuples contiennent beaucoup de code répétitif et sujet aux erreurs : des constructeurs, des accesseurs (getXxx), ainsi que les méthodes equals, hashCode, et toString.

Depuis la version 16 de JAVA, le concept de « record » permet d'écrire simplement des tuples de données. Selon la JEP 395, les objectifs de l'introduction des records comme nouveau type de classes sont les suivants :

  • une construction orientée objet qui exprime une simple agrégation de valeurs,
  • une modélisation de données immuables plutôt que sur un comportement extensible,
  • une mise œuvre automatique des méthodes axées sur les données, telles que les égalités et les accesseurs.

Le projet MiniJAVA comprend beaucoup de classes qui sont des records :

  • pour l'analyse syntaxique, dans le paquetage phase/b_syntax, et dans l'ordre alphabétique, les classes Axiom, ExprArrayLength, ExprArrayLookup, ExprArrayNew, ExprCall, ExprIdent, ExprLiteralBool, ExprLiteralInt, ExprNew, ExprOpBin, ExprOpUn, Formal, Ident, KlassBody, Klass, KlassMain, MethodBody, Method, StmtArrayAssign, StmtAssign, StmtBlock, StmtIf, StmtPrint, StmtWhile, Type, Variable,
  • pour l'analyse sémantique, la classe phase.c_semantic.SemanticTree;
  • pour la génération de la réprésentation intermédiaire, la classe phase.d_intermediate.IntermediateRepresentation, et dans le paquetage phase.d_intermediate.ir et dans l'ordre alphabétique, les classes IRConst, IRLabel, IRTempVar, QAssignArrayFrom, QAssignArrayTo, QAssign, QAssignUnary, QCall, QCallStatic, QCopy, QJumpCond, QJump, QLabel, QLabelMeth, QLength, QNewArray, QNew, QParam, QReturn.

En guise de démonstration, voici le code du record Axiom avec son pendant en version « classe » :

public record Axiom(String label, List<AstNode> enfants, AstLocations locations, KlassMain klassMain, AstList<Klass> klassList) implements AstNode { @Override public void accept(final AstVisitor v) { ... } @Override public String toString() { return print(); } public static Axiom build(final KlassMain klassMain, final AstList<Klass> klassList) { ... } }
public class AxiomClasse implements AstNode { private final String label; private final List<AstNode> enfants; private final AstLocations locations; private final KlassMain klassMain; private final AstList<Klass> klassList; public AxiomClasse(String label, List<AstNode> enfants, AstLocations locations, KlassMain klassMain, AstList<Klass> klassList) { this.label = label; this.enfants = enfants; this.locations = locations; this.klassMain = klassMain; this.klassList = klassList; } public String label() { return label; } public List<AstNode> enfants() { return enfants; } public AstLocations locations() { return locations; } public KlassMain klassMain() { return klassMain; } public AstList<Klass> klassList() { return klassList; } @Override public int hashCode() { return Objects.hash(enfants, klassList, klassMain, label, locations); } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (!(obj instanceof AxiomClasse other)) { return false; } return Objects.equals(enfants, other.enfants) && Objects.equals(klassList, other.klassList) && Objects.equals(klassMain, other.klassMain) && Objects.equals(label, other.label) && Objects.equals(locations, other.locations); } @Override public void accept(final AstVisitor v) { // ... } @Override public String toString() { return print(); } public static AxiomClasse build(final KlassMain klassMain, final AstList<Klass> klassList) { ... } }

Patron de conception Fabrique (Factory)


Soient les records des types des nœuds de l'AST, c'est-à-dire ceux du paquetage phase/b_syntax. Leurs attributs sont classiquement composés de trois ensembles : le label du nœud, les enfants, la position du token dans le texte analysé, puis les attributs spécifiques au nœud (par exemple, le nom de la variable pour la classe ExprIdent), et enfin les attributs repérant les enfants du nœud.

On remarque aussitôt que le passage du paramètre correspondant aux enfants du nœud de l'AST est peu pratique à écrire, par exemple :

RESULT = new ExprOpBin(ExprOpBin.class.getSimpleName(), Arrays.asList(expr1, expr2), new AstLocations(), expr1, op, expr2);

On devine dans cet extrait de code que c'est du code écrit dans la spécification CUP, donc dans le fichier src/main/resources/specifications/minijava.cup, c'est-à-dire dans un fichier écrit sans utiliser un éditeur de code JAVA, ce qui renforce l'intérêt de simplifier l'écriture de la création d'un objet.

C'est le rôle du patron de conception Fabrique d'aider à pallier cette difficulté. Par exemple, dans chacune des classes des nœuds de l'AST, nous ajoutons une « méthode fabrique », dont le rôle est de créer une instance de la classe. Cette méthode permet de simplifier la création d'une instance en appelant new ; on ne trouvera pas d'utilisation de new ailleurs dans le code pour créer des instances de cette classe. Par convention, le nom de cette méthode est build. Voici la méthode fabrique pour le record ExprOpBin :

public static ExprOpBin build(final Expr expr1, final EnumOper op, final Expr expr2) { return new ExprOpBin(ExprOpBin.class.getSimpleName(), Arrays.asList(expr1, expr2), new AstLocations(), expr1, op, expr2); }

Avec cette méthode fabrique, créer un nœud de type ExprOpBin s'écrit comme suit (à comparer avec la ligne de l'extrait de code précédent contenant l'opérateur new) :

RESULT = ExprOpBin.build(expr1, main.EnumOper.AND, expr2);

Ce patron de conception est utilisé dans MiniJAVA de manière systématique pour les classes qui sont des records.

Classes et méthodes de test JUnit

Dans le projet MiniJAVA, pour aider au développement, de nombreux tests sont proposés pour vérifier la correction de la mise en œuvre.

Ces tests sont des tests programmés avec JUnit et exécutés avec la commande mvn test ou dans l'IDE Eclipse avec le menu contextuel Run As > JUnit Test.

Le compilateur est développé de manière itérative et par phase. Par « itératif », on indique que le langage MiniJAVA est construit à partir d'un noyau (le hello word) jusqu'à la compilation de tableaux ; ce sont ce que l'on appelle les « jalons » dans le Memento MiniJAVA. Par « phase », on fait référence aux différentes phases de la compilation : lexicale, syntaxique, etc. Ces deux approches se traduisent comme suit dans l'organisation du code des tests :

  • dans la classe common.SuccessfulMilestonesTest, le tableau jalons définit des exemples simples pour les différents Jalons de développement du compilateur MiniJAVA. Ce tableau est utilisé dans les méthodes protégées jalonStringXX. On fournit aussi des fichiers de test pour les différents jalons : src/test/resources/Jalons/TestXXX.txt, qui sont utilisés dans les méthodes protégées jalonFileXX.
    autant vous pouvez modifier le tableau des jalons, autant on vous demande de ne pas modifier les fichiers TestXXX.txt.
    rien ne vous empêche d'ajouter des méthodes de test, que vous choisissez ensuite de laisser ou de retirer avant livraison.
  • cette même classe common.SuccessfulMilestonesTest définit des méthodes privées utilisées par les autres méthodes de la classe ;
  • la classe de tests common.SuccessfulMilestonesTest est abstraite et ne possède pas de méthodes annotées @Test. En revanche, dans chacun des paquetages phase.*, on trouve une classe concrète SuccessfulMilestonesTest avec les méthodes qui suivent :
    • une méthode d'instance annotée @BeforeEach (setUp) servant à l'initialisation de chaque test. On note que c'est dans cette méthode qu'est contrôlé l'enchaînement des phases de compilation via les méthodes Compiler.doNotStopAfterXxx et Compiler.stopAfterXxx ;
    • une méthode d'instance annotée @AfterEach (il n'y en pas dans la classe SuccessfulMilestonesTest) servant à la libération des données de chaque test ;
    • des méthodes de test annotées @Test. La plupart de ces méthodes sont aussi annotées avec @Disabled pour indiquer à JUnit de ne pas exécuter ces méthodes de test : cette annotation sert à « désactiver » la méthode de test, elle n'est pas exécutée dans la série des tests de la classe. La raison est simple : le développement du compilateur ne permet pas encore que ces tests passent.
      au fur et à mesure du développement du compilateur, ces méthodes « désactivées » sont activées en retirant l'annotation @Disabled.
      toutes les méthodes de test dans les classes concrètes SuccessfulMilestonesTest sont une simple délégation à la méthode redéfinie de la classe abstraite : super.jalonXxx(), ce qui permet de garder la cohérence entre les itérations et les phases.

L'exécution des tests du squelette du projet MiniJAVA est correcte pour le Jalon 1 pour les phases d'analyse lexicale et d'analyse syntaxique. Par exemple :

$ mvn test -Dtest=phase.b_syntax.SuccessfulMilestonesTest#jalonFile1 ... [INFO] Running phase.b_syntax.SuccessfulMilestonesTest === Phase A Analyse Lexicale et Phase B Syntaxique === === new Axiom === Reading file /home/denis/Depots/csc4251-4252/csc4251-4252-compil/GitLab/csc4251_4252-minijava/target/test-classes/Jalons/Test101.txt Parsing OK, Axiom = Axiom[3/1(81)-7/2(173)] = AST Axiom[3/1(81)-7/2(173)] ├─KlassMain[3/1(81)-7/2(173)] │ ├─Ident[3/7(87)-3/14(94)] Test101 │ ├─Ident[4/36(132)-4/40(136)] args │ └─StmtPrint[5/5(144)-5/28(167)] │ └─ExprLiteralInt[5/24(163)-5/26(165)] 42 └─AstList = Pretty Print class Test101{ public static void main(String [] args) { System.out.println(42); } } 🚧 Fin provisoire de Compilation

N.B. 1 : remarquer que, dans les méthodes de test programmées et proposées pour le compilateur MiniJAVA, les tests consistent à vérifier que la compilation d'un programme MiniJAVA ne lève pas d'exception : cf. l'instruction Assertions.assertDoesNotThrow qui vérifie qu'il n'y a pas d'exception levée.

N.B. 2 : une conséquence importante de la précédente note est que, lors du développement du compilateur MiniJAVA, il faudra vérifier via l'affichage dans la console que tout s'est bien passé : liste des tokens, arbre de syntaxe concret, arbre de syntaxe abstrait, etc. jusqu'à l'affichage de l'exécution du programme MIPS généré qui doit être correct. En effet, on ne construit pas de tests avec vérification de la compilation : autrement dit, pas d'instructions Assertions.assertEquals dans les méthodes de test.