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 create(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 create(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 :

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

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 create. Voici la méthode fabrique pour le record ExprOpBin :

public static ExprOpBin create(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.create(a, main.EnumOper.AND, z);

On devine dans le dernier 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.

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 tests 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.

Pour rappeler ou expliquer le fonctionnement de JUnit vu dans le module PRO3600, étudions la classe test.SuccessfulMilestonesTest. Cette classe de tests (avec une série de tests) définit classiquement :

  • des attributs utilisés par les méthodes qui suivent : par exemple le tableau jalons qui définit des exemples simples pour les différents Jalons de développement du compilateur MiniJAVA ;
  • des méthodes privées utilisées par les autres méthodes de la classe ;
  • une méthode d'instance annotée @BeforeEach (setUp) servant à l'initialisation de chaque test ;
  • 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 : par exemple, au départ du projet MiniJAVA, les méthodes jalonString1 et jalonFile1. Remarquer que l'annotation @DisplayName permet d'indiquer le rôle de la méthode de test.

Au départ du projet MiniJAVA, la plupart des méthodes de test, c.-à-d. avec l'annotation @Test, possèdent aussi l'annotation @Disabled. Cette dernière 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. Ces méthodes « désactivées » sont activées au fur et à mesure du développement du compilateur en retirant l'annotation @Disabled.

Si on considère la classe de tests JUnit SuccessfulMilestonesTest au début du projet MiniJAVA, l'exécution de la série de tests donne ce qui suit :

@BeforeEach setUp @Test jalonString1 // si on ajoutait une méthode @AfterEach tearDown @BeforeEach setUp @Test jalonFile1 // si on ajoutait une méthode @AfterEach tearDown

N.B. 1 : remarquer que, dans les méthodes de tests 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é : l'AST est bien formé, etc. jusqu'à l'affichage de l'exécution du programme MIPS généré qui doit être correct.