Mémento JAVA
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.
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 :
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++ ».
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 :
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 :
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) :
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 :
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.