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

Portail informatique

Compilateur MiniJAVA vers MIPS

La documentation commune aux différentes phases 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 $ cp -r csc4251_4252-minijava csc4251_4252-minijava-original # on garde une copie pour comparer en cas de difficulté $ cd csc4251_4252-minijava $ ls Docs lib pom.xml readme.md src $ emacs pom.xml # IMPORTANT : adapter le nom de l'artefact Maven minijava-NOM1_PRENOM1-NOM2_PRENOM2
  • Si besoin revoir le prologue des exercices JFlex+CUP pour le développement JAVA avec Maven avec l'IDE Eclipse.
  • Intégrer les deux arborescences comme projets 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.

En guise de démonstration et pour donner plus d'explications, prenons l'exemple de la commande mvn test -Dtest=test.SuccessfulMilestonesTest#jalonFile1 (après avoir exécuter la commande mvn clean install), ou dans l'IDE Eclipse, sélectionner la méthode jalonFile1 dans la classe et utiliser le menu contextuel Run As > JUnit Test. Remarquer (1) la commande mvn test utilisée pour exécuter les tests JUnit sans re-générer et re-compiler le code, et (2) l'option JAVA -Dtest= utilisée pour spécifier l'unique méthode de test à exécuter : dans le paquetage test, la classe SuccessfulMilestonesTest, la méthode jalonFile1. Voici l'affichage de l'exécution :

[INFO] Running test.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[2/1(27)-6/2(119)] = AST Axiom[2/1(27)-6/2(119)] ├─KlassMain[2/1(27)-6/2(119)] │ ├─Ident[2/7(33)-2/14(40)] Test101 │ ├─Ident[3/36(78)-3/40(82)] args │ └─StmtPrint[4/5(90)-4/28(113)] │ └─ExprLiteralInt[4/24(109)-4/26(111)] 42 └─AstList = Pretty Print class Test101{ public static void main(String [] args) { System.out.println(42); } } 🚧 Fin provisoire de Compilation [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.080 s -- in test.SuccessfulMilestonesTest

On reconnaît l'impression à l'écran de l'AST. Noter la fin de l'exécution avec le message « 🚧 Fin provisoire de Compilation ». Ce message signifie que le compilateur n'a pas exécuté toutes les phases de compilation : cf. l'instruction Compiler.stopAfterSyntax() dans la méthode compil.Compiler::execute. Donc, lorsque le développement du compilateur MiniJAVA avance d'une phase, penser à modifier la méthode compil.Compiler::execute pour exécuter la nouvelle phase développée.

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.

Demande Importante : le projet MiniJAVA est développé sans gestion de version. Or, beaucoup des classes fournies dans le squelette au départ du projet n'ont a priori pas besoin d'être modifiées. Par conséquent, lorsqu'une classe est modifiée sans que cela ne fasse a priori partie des consignes des exercices (c'est une classe qui n'est pas identifiée comme « à compléter »), ajouter en commentaire JavaDoc la mention suivante : « // NOM, date ».

Analyse lexicale et syntaxique

Préliminaire


Le compilateur fourni est fonctionnel mais uniquement pour une syntaxe très minimaliste (« System.out.println(42); » dans le fichier ./src/test/resources/Jalons/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). La phase 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 phases 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 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 AstNode::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/Jalons/.
      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 la construction par jalon de la spécification JFlex+CUP :

  • 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),
  • 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.

Une paire de spécification JFlex et CUP :

Attention aux potentiels « ... » dans le code inséré !

... /// Macros WS = [ \t\f] | \R EOLComment = "//" .* C89Comment = "/*" [^*]* ("*" ([^*/] [^*]*)?)* "*/" Ignore = {WS} | {EOLComment} | {C89Comment} Integer = 0 | [1-9] [0-9]* Boolean = "true" | "false" Ident = [:jletter:] [:jletterdigit:]* %% //// Mots Clés "boolean" { return TOKEN(BOOLEAN); } "class" { return TOKEN(CLASS); } "else" { return TOKEN(ELSE); } "extends" { return TOKEN(EXTENDS); } "if" { return TOKEN(IF); } "int" { return TOKEN(INT); } "length" { return TOKEN(LENGTH); } "main" { return TOKEN(MAIN); } "new" { return TOKEN(NEW); } "out" { return TOKEN(OUT); } "println" { return TOKEN(PRINTLN); } "public" { return TOKEN(PUBLIC); } "return" { return TOKEN(RETURN); } "static" { return TOKEN(STATIC); } "String" { return TOKEN(STRING); } "System" { return TOKEN(SYSTEM); } "void" { return TOKEN(VOID); } "while" { return TOKEN(WHILE); } ... ... //// Operateurs "&&" { return TOKEN(AND); } "=" { return TOKEN(ASSIGN); } "<" { return TOKEN(LESS); } "-" { return TOKEN(MINUS); } "!" { return TOKEN(NOT); } "+" { return TOKEN(PLUS); } "*" { return TOKEN(TIMES); } ... ... //// Ponctuations "," { return TOKEN(COMMA); } "." { return TOKEN(DOT); } ";" { return TOKEN(SEMI); } "[" { return TOKEN(LBRACK); } "]" { return TOKEN(RBRACK); } "{" { return TOKEN(LBRACE); } "}" { return TOKEN(RBRACE); } "(" { return TOKEN(LPAREN); } ")" { return TOKEN(RPAREN); } //// Literals, Identificateurs {Integer} { return TOKEN(LIT_INT, Integer.parseInt(yytext())); } {Boolean} { return TOKEN(LIT_BOOL, Boolean.parseBoolean(yytext())); } {Ident} { return TOKEN(IDENT, new String(yytext())) ; } //// Ignore, Ramasse Miette {Ignore} { } [^] { WARN("Invalid char '" + yytext() + "'"); return TOKEN(error); } ...

Attention aux potentiels « ... » dans le code inséré !

... package phase.b_syntax; import phase.b_syntax.ast.*; // définition de l'AST import solutiontableauxetboni.*; /* TORMVSQUEL */ action code {: :}; /* Lexèmes (Tokens) */ terminal CLASS, MAIN, OUT, PRINTLN, PUBLIC, STATIC, STRING, SYSTEM, VOID; terminal DOT, SEMI, LBRACE, RBRACE, LPAREN, RPAREN; terminal BOOLEAN, ELSE, EXTENDS, IF, INT, NEW, RETURN, WHILE; terminal AND, ASSIGN, LESS, MINUS, NOT, PLUS, TIMES, COMMA; ... ... /* Lexèmes avec valeur sémantique */ terminal Integer LIT_INT; terminal Boolean LIT_BOOL; terminal String IDENT; /* Variables de la grammaire et Arbre de syntaxe */ nonterminal Axiom axiom; nonterminal KlassMain klassMain; nonterminal Ident ident; nonterminal AstList<Klass> klassList; nonterminal Klass klass; nonterminal Ident parent; nonterminal KlassBody klassBody; nonterminal Method method; nonterminal MethodBody methBody; nonterminal Type type; nonterminal Variable variable; nonterminal Formal formal; nonterminal AstList<Formal> formals, formals1; nonterminal AstList<Expr> args, args1; nonterminal Stmt stmt; nonterminal Expr expr; /* Associativités et Priorités */ precedence right ASSIGN; /* prec 1 */ precedence left AND; /* prec 4 */ ... ... precedence left LESS; /* prec 9 */ precedence left PLUS, MINUS; /* prec 11 */ precedence left TIMES; /* prec 12 */ precedence right NOT; /* prec 13 */ precedence left DOT, LBRACK, RBRACK, LPAREN, RPAREN; /* prec 15 */ /* Règles de Productions */ axiom ::= klassMain:a klassList:z {: RESULT = Axiom.create(a, z); RESULT.addPosition(axleft, zxright); :} ; klassMain ::= CLASS:a ident:b LBRACE PUBLIC STATIC VOID MAIN LPAREN STRING LBRACK RBRACK ident:c RPAREN LBRACE stmt:d RBRACE RBRACE:z {: RESULT = KlassMain.create(b, c, d); RESULT.addPosition(axleft, zxright); :} ; ident ::= IDENT:a {: RESULT = Ident.create(a); RESULT.addPosition(axleft, axright); :} ; klassList ::= /* vide */ {: RESULT = new AstList<>(); :} | klassList:a klass:z {: RESULT = a; RESULT.add(z); RESULT.addPosition(axleft, zxright); :} ; klass ::= CLASS:a ident:b parent:c LBRACE klassBody:d RBRACE:z {: RESULT = Klass.create(b, c, d.attributs(), d.methodes()); RESULT.addPosition(axleft, zxright); :} ; parent ::= /* vide */ {: RESULT = Ident.create("Object"); :} | EXTENDS:a ident:z {: RESULT = z; RESULT.addPosition(axleft, zxright); :} ; klassBody ::= /* vide */ {: RESULT = KlassBody.create(); :} | klassBody:a variable:z {: RESULT = a; RESULT.attributs().add(z); :} | klassBody:a method:z {: RESULT = a; RESULT.methodes().add(z); :} ; method ::= PUBLIC:a type:b ident:c LPAREN formals:d RPAREN LBRACE methBody:e RETURN expr:f SEMI RBRACE:z {: RESULT = Method.create(b, c, d, e.vars(), e.instructions(), f); RESULT.addPosition(axleft, zxright); :} ; methBody ::= /* vide */ {: RESULT = MethodBody.create(); :} | methBody:a variable:z {: RESULT = a; RESULT.vars().add(z); :} | methBody:a stmt:z {: RESULT = a; RESULT.instructions().add(z); :} ; variable ::= type:a ident:b SEMI:z {: RESULT = Variable.create(a, b); RESULT.addPosition(axleft, zxright); :} ; type ::= IDENT:a {: RESULT = Type.create(a); RESULT.addPosition(axleft, axright); :} | INT:a {: RESULT = Type.create(compil.EnumType.INT); RESULT.addPosition(axleft, axright); :} | BOOLEAN:a {: RESULT = Type.create(compil.EnumType.BOOL); RESULT.addPosition(axleft, axright); :} ; // Listes éventuellement vides avec Virgule formal ::= type:a ident:z {: RESULT = Formal.create(a, z); RESULT.addPosition(axleft, zxright); :} ; formals ::= /* vide */ {: RESULT = new AstList<>(); :} | formals1:a {: RESULT = a; RESULT.addPosition(axleft, axright); :} ; formals1 ::= formal:a {: RESULT = new AstList<>(); RESULT.add(a); RESULT.addPosition(axleft, axright); :} | formals1:a COMMA formal:z {: RESULT = a; RESULT.add(z); RESULT.addPosition(axleft, zxright); :} ; args ::= /* vide */ {: RESULT = new AstList<>(); :} | args1:a {: RESULT = a; RESULT.addPosition(axleft, axright); :} ; args1 ::= expr:a {: RESULT = new AstList<>(); RESULT.add(a); RESULT.addPosition(axleft, axright); :} | args1:a COMMA expr:z {: RESULT = a; RESULT.add(z); RESULT.addPosition(axleft, zxright); :} ; // Instructions stmt ::= SYSTEM:a DOT OUT DOT PRINTLN LPAREN expr:b RPAREN SEMI:z {: RESULT = StmtPrint.create(b); RESULT.addPosition(axleft, zxright); :} | ident:a ASSIGN expr:b SEMI:z {: RESULT = StmtAssign.create(a, b); RESULT.addPosition(axleft, zxright); :} | LBRACE:a methBody:b RBRACE:z {: RESULT = StmtBlock.create(b.vars(), b.instructions()); RESULT.addPosition(axleft, zxright); :} | IF:a LPAREN expr:b RPAREN stmt:c ELSE stmt:z {: RESULT = StmtIf.create(b, c, z); RESULT.addPosition(axleft, zxright); :} | WHILE:a LPAREN expr:b RPAREN stmt:z {: RESULT = StmtWhile.create(b, z); RESULT.addPosition(axleft, zxright); :} ; // Expressions expr ::= LIT_INT:a {: RESULT = ExprLiteralInt.create(a); RESULT.addPosition(axleft, axright); :} | LIT_BOOL:a {: RESULT = ExprLiteralBool.create(a); RESULT.addPosition(axleft, axright); :} | LPAREN:a expr:b RPAREN:z {: RESULT = b; RESULT.addPosition(axleft, zxright); :} | expr:a AND expr:z {: RESULT = ExprOpBin.create(a, compil.EnumOper.AND, z); RESULT.addPosition(axleft, zxright); :} | expr:a LESS expr:z {: RESULT = ExprOpBin.create(a, compil.EnumOper.LESS, z); RESULT.addPosition(axleft, zxright); :} | expr:a PLUS expr:z {: RESULT = ExprOpBin.create(a, compil.EnumOper.PLUS, z); RESULT.addPosition(axleft, zxright); :} | expr:a MINUS expr:z {: RESULT = ExprOpBin.create(a, compil.EnumOper.MINUS, z); RESULT.addPosition(axleft, zxright); :} | expr:a TIMES expr:z {: RESULT = ExprOpBin.create(a, compil.EnumOper.TIMES, z); RESULT.addPosition(axleft, zxright); :} | NOT:a expr:z {: RESULT = ExprOpUn.create(compil.EnumOper.NOT, z); RESULT.addPosition(axleft, zxright); :} | expr:a DOT ident:b LPAREN args:c RPAREN:z {: RESULT = ExprCall.create(a, b, c); RESULT.addPosition(axleft, zxright); :} | ident:a {: RESULT = ExprIdent.create(a); RESULT.addPosition(axleft, axright); :} | NEW:a ident:b LPAREN RPAREN:z {: RESULT = ExprNew.create(b); RESULT.addPosition(axleft, zxright); :} | MINUS LIT_INT:a {: RESULT = ExprLiteralInt.create(- a); RESULT.addPosition(axleft, axright); :} ; ...

Nous conseillons de faire le travail autant que faire se peut sans regarder les éléments de réponse fournis (pour comprendre la grammaire BNF ainsi que la structure du code fourni), puis de compléter avec la solution (pour éviter d'y passer trop de temps). Et, pensez à tenir à jour le fichier binome_note_travail_sur_le_projet.md.

Quelques questions sur l'analyse lexicale et l'analyse syntaxique


Dans le fichier binome_note_travail_sur_le_projet.md, copiez les énoncés et répondez aux questions ; pensez à fournir quelques explications. Dans un programme MiniJAVA :

  1. est-il possible de définir la valeur d'un entier sous la forme octale ?
  2. dans une méthode, peut-on écrire « int a; a = 0; int b; b = n; » ? ou doit-on plutôt écrire « int a; int b; a = 0; b = n; » ?
  3. est-il possible de mettre un attribut de classe (static) dans la classe qui contient la méthode main ?
  4. est-il possible d'avoir une variable locale dans la méthode main ?

Analyse lexicale et analyse syntaxique du Projet MiniJAVA en autonomie : On ajoute les tableaux


Les éléments pour ajouter les tableaux à la grammaire existante, c'est-à-dire pour passer du Jalon 7 au Jalon 9, sont disponibles à la fin de cette page dans la section intitulée « Projet MiniJAVA en autonomie : On ajoute les tableaux ».

Résumé des actions À FAIRE dans cette phase

  1. ❑ Répondre aux questions posées dans le fichier binome_note_travail_sur_le_projet.md
  2. ❑ Réaliser les spécifications JFlex+CUP pour les Jalons 2–7, y compris l'appel au visiteur PrettyPrint (pour voir dans la console le résultat de l'analyse lexicale et de l'analyse syntaxique) ainsi que les tests (intégrés à la construction du compilateur et qui « passent »).
  3. ❑ Réaliser l'analyse lexicale ainsi que l'analyse syntaxique du Projet MiniJAVA, c'est-à-dire pour avoir aussi des tableaux.
  4. ❑ Indiquer le statut des tâches pour cette phase dans le fichier binome_note_travail_sur_le_projet.md

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 la solution.

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 la phase d'analyse sémantique.

Petite Visite, contre-visite, et fusion


É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. DisplayVarDeclarations commence comme suit :

public class DisplayVarDeclarations extends AstVisitorDefault { /** * Le Writer pour impression. */ private final IndentWriter out = new IndentWriter(); /** * L'attribut servant à repérer lorsque l'on est à l'intérieur d'une méthode. */ private boolean insideMethod; /** * Constructeur. * * @param semTree l'arbre sémantique. */ public DisplayVarDeclarations(final SemanticTree semTree) { out.print("= Affichage des Identificateurs : "); insideMethod = false; semTree.axiom().accept(this); Debug.log(out); } ...

L'appel au visiteur est inclut dans phase.c_semantic.Semantic::execute : 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::execute :

public SemanticTree execute() { // 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 ./src/test/resources/Jalons/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.

Voici le visiteur :

Attention aux potentiels « ... » dans le code inséré !

... /////////////////// Visit //////////////////// @Override public void visit(final Formal n) { out.print(n.varId().name() + "(formal), "); defaultVisit(n); } @Override public void visit(final Variable n) { out.print(n.varId().name() + (insideMethod ? "(local)" : "(field)") + ", "); defaultVisit(n); } @Override public void visit(final Method n) { out.print(n.methodId().name() + "(method), "); insideMethod = true; defaultVisit(n); insideMethod = false; } @Override public void visit(final Klass n) { out.print(n.klassId().name() + " (klasse), "); defaultVisit(n); } } ...

É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/Jalons/Test105.txt :

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

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

Voici le visiteur :

Attention aux potentiels « ... » dans le code inséré !

... @Override public void visit(final Klass n) { out.print(n.klassId().name() + "{"); defaultVisit(n); out.print("}"); } @Override public void visit(final Method n) { out.print(n.methodId().name() + "{"); defaultVisit(n); out.print("}"); } @Override public void visit(final StmtBlock n) { out.print("{"); defaultVisit(n); out.print("}"); } } ...

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/Jalons/Test105.txt :

= Affichage des identificateurs dans leur portée : Test2{a(field), b(field), Start{i(formal), j(formal), k(local), }un{}}Test3{zero{}}

Attention aux potentiels « ... » dans le code inséré !

... /////////////////// Visit //////////////////// @Override public void visit(final Formal n) { out.print(n.varId().name() + "(formal), "); defaultVisit(n); } @Override public void visit(final Variable n) { out.print(n.varId().name() + (insideMethod ? "(local)" : "(field)") + ", "); defaultVisit(n); } @Override public void visit(final Method n) { out.print(n.methodId().name() + "{"); insideMethod = true; defaultVisit(n); insideMethod = false; out.print("}"); } @Override public void visit(final Klass n) { out.print(n.klassId().name() + "{"); defaultVisit(n); out.print("}"); } @Override public void visit(final StmtBlock n) { out.print("{"); defaultVisit(n); out.print("}"); } } ...

Table des Symboles


Outre la section 7 du 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, car 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 BuildSymTab::execute 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. Ainsi, l'affichage à la console de la table des symboles contiendra les lignes suivantes :
    Scope Root Scope Object boolean equals(Object this, Object o) Scope equals_args Object this Object o Scope equals

É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).

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 KlassMain, 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.

En guise d'exemple, voici la redéfinition de la visite par défaut ainsi que de la visite pour le nœud KlassMain :

/** * Redéfinition de la visite par défaut : Intégration de l'héritage par défaut * des attributs hérités (Scope, Klass). */ @Override public void defaultVisit(final AstNode n) { setKlass(n, currentKlass); setScope(n, currentScope); for (AstNode f : n) { f.accept(this); } currentKlass = getKlass(n); currentScope = getScope(n); }
/** * Déclaration de la classe avec la méthode main de Minijava. Pour l'exemple, * mais inutile en pratique car on ne fera rien avec dans la suite. */ @Override public void visit(final KlassMain n) { setKlass(n, currentKlass); setScope(n, currentScope); currentKlass = new InfoKlass(n.klassId().name(), OBJECT); currentScope = newKlassScope(currentScope, currentKlass); n.klassId().accept(this); n.argId().accept(this); final InfoMethod m = new InfoMethod("void", "main", new InfoVar(n.argId().name(), "String[]")); currentScope = newMethodScope(currentScope, m); n.stmt().accept(this); currentKlass = getKlass(n); currentScope = getScope(n); }

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

= Table des symboles (passe1) Scope Root class Object extends null class Test103 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 Test103 void main(String[] args) Scope main_args String[] args Scope main Scope Test2 int Start(Test2 this) Scope Start_args Test2 this Scope Start Scope Test3 int zero(Test3 this) Scope zero_args Test3 this Scope zero

Attention aux potentiels « ... » dans le code inséré !

... /** * Redéfinition de la visite par défaut : Intégration de l'héritage par défaut * des attributs hérités (Scope, Klass). */ @Override public void defaultVisit(final AstNode n) { setKlass(n, currentKlass); setScope(n, currentScope); for (AstNode f : n) { f.accept(this); } currentKlass = getKlass(n); currentScope = getScope(n); } // Visites Spécifiques : // - Création de portées : KlassMain, Klass, Method, StmtBlock // - Déclarations : KlassMain, Klass, Method, Variable, Formal (in Method) // Visites non écrites dans le squelette de MiniJAVA @Override public void visit(final Klass n) { setKlass(n, currentKlass); setScope(n, currentScope); currentKlass = new InfoKlass(n.klassId().name(), n.parentId().name()); this.currentScope = newKlassScope(currentScope, currentKlass); n.klassId().accept(this); n.parentId().accept(this); n.vars().accept(this); n.methods().accept(this); currentKlass = getKlass(n); currentScope = getScope(n); } @Override public void visit(final Method n) { setKlass(n, currentKlass); setScope(n, currentScope); // les paramètres formels explicites final InfoVar[] formals = new InfoVar[n.fargs().nbChildren() + 1]; int index = 1; for (AstNode f : n.fargs()) { formals[index] = new InfoVar(((Formal) f).varId().name(), ((Formal) f).typeId().name()); index++; } // le paramètre implicite "this" formals[0] = new InfoVar("this", getKlass(n).getName()); // la définition de la méthode final InfoMethod m = new InfoMethod(n.returnType().name(), n.methodId().name(), formals); currentScope = newMethodScope(currentScope, m); n.returnType().accept(this); n.methodId().accept(this); n.fargs().accept(this); // ne fait rien ! n.vars().accept(this); n.stmts().accept(this); n.returnExp().accept(this); currentKlass = getKlass(n); currentScope = getScope(n); } @Override public void visit(final Variable n) { setKlass(n, currentKlass); setScope(n, currentScope); final InfoVar v = new InfoVar(n.varId().name(), n.typeId().name()); checkRedef(getScope(n).insertVariable(v)); currentKlass = getKlass(n); currentScope = getScope(n); } @Override public void visit(final StmtBlock n) { setKlass(n, currentKlass); setScope(n, currentScope); currentScope = new Scope(currentScope); n.vars().accept(this); n.stmts().accept(this); currentKlass = getKlass(n); currentScope = getScope(n); } } ...

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

Voici un exemple de résultat d'exécution sur le fichier exemple ./src/test/resources/Jalons/Test103.txt. Comme montré dans le tableau qui suit, pour que la classe enfant Test3 inclut les identificateurs de la classe parente Test2, la portée de la classe enfant est « déplacée » dans la portée de la classe parente : ce déplacement est opéré par la méthode Scope::addInheritance.

= Table des symboles (passe1) Scope Root ... Scope Test2 int Start(Test2 this) Scope Start_args Test2 this Scope Start Scope Test3 int zero(Test3 this) Scope zero_args Test3 this Scope zero
= Table des Symboles (passe2) Scope Root ... Scope Test2 int Start(Test2 this) Scope Start_args Test2 this Scope Start Scope Test3 int zero(Test3 this) Scope zero_args Test3 this Scope zero

Les méthodes de test test403SemanticError1 et test403SemanticError2 de test de la classe de tests JUnit test.ErrorsSemanticsInheritanceTest montrent deux exécutions avec un cycle dans l'arbre d'héritage :

## test403SemanticError1 = Table des symboles (passe1) Scope Root ... class G extends H class H extends G ... Loop in ancestors from class class G extends H Loop in ancestors from class class H extends G ... CheckInheritance : Héritage JAVA invalide ...
## test403SemanticError2 = Table des symboles (passe1) Scope Root ... class F extends F ... Loop in ancestors from class class F extends F ... CheckInheritance : Héritage JAVA invalide ...

La solution est déjà fournie dans le code dans la classe phase.c_semantic.CheckInheritance !

Allready defined


Écrire la fonction sémantique Identifier allready defined.

Comme indiqué lors de la construction de la table des symboles, on utilise la méthode BuildSymTab::checkRedef. C'est donc déjà fait dans le travail de section 2.3 !

(Bonus 1) Undefined et Unused


Écrire un nouveau visiteur phase.c_semantic.VarUndefUnused 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.

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. Faire attention au cas particulier de la variable this qui a dû être systématiquement ajoutée comme premier paramètre dans les méthodes.

Les méthodes visit à redéfinir sont celles pour les types ExprCall, ExprNew, ExprIdent, StmtAssign, StmtArrayAssign (pour les tableaux), et Method (attendre après le contrôle de type).

Gestion d'erreurs


Cf. la section 5 du Mémento MiniJAVA ainsi que le code fourni...

Quelques questions sur la construction de la table des symboles dans l'analyse sémantique


Dans le fichier binome_note_travail_sur_le_projet.md, copiez les énoncés et répondez aux questions ; pensez à fournir quelques explications :

  1. lorsque l'on écrit un visiteur héritant du visiteur par défaut AstVisitorDefault, à quoi sert l'appel à la méthode defaultVisit dans les méthodes redéfinies ?
  2. lorsque l'on ajoute un nouveau type de nœud dans l'AST, faut-il modifier le visiteur par défaut AstVisitorDefault ? si oui, pourquoi ?
  3. que fait notre compilateur en cas d'erreur dans la gestion de l'héritage (détection d'un cycle) ?
  4. notre compilateur considère-t-il comme une erreur de redéfinition l'écrasement d'un paramètre d'appel par une variable locale ? Qu'en est-il du compilateur javac ?

Analyse sémantique — Construction de la table des symboles du Projet MiniJAVA en autonomie : On ajoute les tableaux


Les éléments pour ajouter les tableaux à la construction de la table des symboles de l'analyse sémantique existante, c'est-à-dire pour passer du Jalon 7 au Jalon 9, sont disponibles à la fin de cette page dans la section intitulée « Projet MiniJAVA en autonomie : On ajoute les tableaux ».

Résumé des actions À FAIRE dans cette phase

  1. ❑ Répondre aux questions posées dans le fichier binome_note_travail_sur_le_projet.md
  2. ❑ Réaliser la construction de la table des symboles (visiteur BuildSymTab, mais aussi les visiteurs DisplayVarDeclarations, DisplayScopes, et DisplayVarDeclarationsInScopes) ainsi que les tests (intégrés à la construction du compilateur et qui « passent »). Attention : vérifier aussi les affichages ! Par exemple avec les exemples donnés dans l'énoncé.
  3. ❑ Réaliser la construction de la table des symboles du Projet MiniJAVA, c'est-à-dire pour avoir aussi des tableaux.
  4. ❑ Indiquer le statut des tâches pour cette phase dans le fichier binome_note_travail_sur_le_projet.md

Analyse Sémantique : Contrôle de type

hors bonus, cette étape est sans conteste la plus consommatrice de temps et la plus intéressante.
on aura intérêt à considérer ensemble les cousins Type Top et Top Type.

Préliminaire


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. méthode phase.c_semantic.symtab.Scope::lookup*).

Ce schéma fonctionne pour les méthodes de classe (static) mais ne fonctionne pas pour les méthodes d'instance. En effet, pour une méthode d'instance, la recherche ne dépend pas de la portée courante mais dépend de la classe concrète du receveur (this). Pour x.get(), la déclaration de get() est à chercher à partir de la portée de la classe concrète de x.

Par conséquent, dans le cas d'une liaison statique (cas pour MiniJAVA), on considère le type formel (déclaré) de x qui est connu lors de l'analyse sémantique (contrôle de type). Dans le cas d'une liaison dynamique (JAVA), on considère le type actuel (réel) de x (celui de l'instanciation avec new). La liaison dynamique est donc potentiellement réalisée à l'exécution ou dans une phase d'analyse sémantique dynamique (flot d'exécution et coutures d'arbres).

Type Top : calcul de l'attribut Type


É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 serait aussi nécessaire 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.

Comme indiquée au début de la phase, le travail à faire est conséquent. En effet, on doit redéfinir la visite pour la plupart des nœuds de l'AST, en l'occurrence :

  • les nœuds Expr* pour positionner l'attribut de type ;
  • la plupart des nœuds Stmt* et Expr* pour vérifier (méthode checkType) la compatibilité des types ;
  • le nœud Type pour valider (avec la méthode checkTypeName) des noms de type dans les nœuds enfants des nœuds Variable, Method et Formal ;
  • le nœud Method pour vérifier la compatibilité du type de retour (type de returnExpr).

En guise d'aide, cf. le pseudo-code de la visite des nœuds de type ExprCall dans la section qui suit.

cf. question suivante.

Top Type : contrôle de 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 transtypage pour les types primitifs. Pour les types Objet, 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 (méthode récursive compareType) 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 identificateurs, 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 à 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.

En guise d'aide, voici le pseudo-code de la visite des nœuds de type ExprCall :

visit(ExprCall n) visit du nœud enfant receiver à partir du nœud receiver, obtenir le InfoKall kl (getType puis lookupKlass) si kl == null, pb visit du nœud enfant methodId à partir du nœud methodId, obtenir le InfoMethod mthd (getScope de kl puis lookupMethod dans la portée de kl) si mthd == null, pb si length de args de mthd != nb enfants de n + 1, pb // attention à this mis en arg0 ! pour tout enfant e de n.args faire visite de e vérifier type e avec correspondant dans mthd.getArgs type de n est type de retour de mthd

Attention aux potentiels « ... » dans le code inséré !

... /////////////////// Visit //////////////////// // Visites spécifiques : // - Expr* : Positionner l'attribut Type // - Stmt* + Expr* (sauf exceptions) : Compatibilité des Types // - Type : Validité des noms de type dans Variable, Method, Formal // - Method : returnType compatible avec Type(returnExpr) // Déclarations @Override public void visit(final Method n) { defaultVisit(n); checkType(n.returnType().name(), getType(n.returnExp()), "Invalid Type for return in method " + n.methodId(), n); } @Override public void visit(final Type n) { defaultVisit(n); checkTypeName(n.name(), n); } // Expressions @Override public void visit(final ExprLiteralInt n) { setType(n, INT); } @Override public void visit(final ExprLiteralBool n) { setType(n, BOOL); } @Override public void visit(final ExprCall n) { n.receiver().accept(this); final InfoKlass kl = lookupKlass(getType(n.receiver())); if (kl == null) { erreur(n, "Attempt to call a non-method (unknown class)"); setType(n, VOID); return; } n.methodId().accept(this); final InfoMethod m = kl.getScope().lookupMethod(n.methodId().name()); /* * NB : pour gérer la surcharge, il faudrait faire un lookupAllMethods et itérer * sur la liste des méthodes pour trouver la première méthode qui passe le * contrôle de type ... */ if (m == null) { erreur(n, "Attempt to call a non-method (unknown method)"); setType(n, VOID); return; } if (m.getArgs().length != n.args().nbChildren() + 1) { // attention à this mis en arg0 erreur(n, "Call of method " + m + " does not match the number of args"); setType(n, VOID); return; } int i = 1; for (AstNode f : n.args()) { f.accept(this); checkType(m.getArgs()[i++].type(), getType(f), "Call of method does not match the signature :" + m, n); } setType(n, m.getReturnType()); } @Override public void visit(final ExprIdent n) { defaultVisit(n); setType(n, lookupVarType(n, n.varId().name())); } @Override public void visit(final ExprNew n) { defaultVisit(n); checkTypeName(n.klassId().name(), n); setType(n, n.klassId().name()); } @Override public void visit(final ExprOpBin n) { defaultVisit(n); final String msg1 = "Invalid Type for left arg " + n.op(); final String msg2 = "Invalid Type for right arg " + n.op(); switch (n.op()) { case PLUS, MINUS, TIMES: checkType(INT, getType(n.expr1()), msg1, n); checkType(INT, getType(n.expr2()), msg2, n); setType(n, INT); break; case AND: checkType(BOOL, getType(n.expr1()), msg1, n); checkType(BOOL, getType(n.expr2()), msg2, n); setType(n, BOOL); break; case LESS: checkType(INT, getType(n.expr1()), msg1, n); checkType(INT, getType(n.expr2()), msg2, n); setType(n, BOOL); break; default: setType(n, VOID); break; } } @Override public void visit(final ExprOpUn n) { defaultVisit(n); final String msg = "Invalid Type for operator " + n.op(); if (n.op() == compil.EnumOper.NOT) { checkType(BOOL, getType(n.expr()), msg, n); setType(n, BOOL); } else { setType(n, VOID); } } // Instructions @Override public void visit(final StmtPrint n) { defaultVisit(n); checkType(INT, getType(n.expr()), "non integer for printing", n); } @Override public void visit(final StmtAssign n) { defaultVisit(n); checkType(lookupVarType(n, n.varId().name()), getType(n.value()), "Type mismatch in assignment", n); } @Override public void visit(final StmtBlock n) { defaultVisit(n); } @Override public void visit(final StmtIf n) { defaultVisit(n); checkType(BOOL, getType(n.test()), "non boolean as condition of if", n); } @Override public void visit(final StmtWhile n) { defaultVisit(n); checkType(BOOL, getType(n.test()), "non boolean as condition of while", n); } } ...

Bonus 1 (redite)


Terminer et valider les questions « Undefined » et « Unused » de la phase précédente (phase 2) en intégrant la recherche des méthodes. On ne traite que la liaison statique et l'on ignore le polymorphisme des méthodes.

Quelques questions sur le contrôle de type dans l'analyse sémantique


Dans le fichier binome_note_travail_sur_le_projet.md, copiez les énoncés et répondez aux questions ; pensez à fournir quelques explications :

  1. Le compilateur MiniJAVA gère-t-il la surcharge ? Si ce n'est pas le cas, que fait le compilateur ? Pour cette question, cf. aussi la section 2.4 du Mémento MiniJAVA ;
  2. Le compilateur MiniJAVA gère-t-il la redéfinition ? la liaison dynamique ? Si ce n'est pas le cas, que fait le compilateur ? Pour cette question, cf. aussi la section 2.4 du Mémento MiniJAVA ;
  3. Dans le code du visiteur TypeChecking, comment voit-on que le type est un attribut synthétisé ?
  4. Pourquoi la méthode compareType appelée par la méthode checkType est-elle récursive ?

Analyse sémantique — Contrôle de type du Projet MiniJAVA en autonomie : On ajoute les tableaux


Les éléments pour ajouter les tableaux au contrôle de type de l'analyse sémantique existante, c'est-à-dire pour passer du Jalon 7 au Jalon 9, sont disponibles à la fin de cette page dans la section intitulée « Projet MiniJAVA en autonomie : On ajoute les tableaux ».

Résumé des actions À FAIRE dans cette phase

  1. ❑ Répondre aux questions posées dans le fichier binome_note_travail_sur_le_projet.md
  2. ❑ Réaliser le contrôle de type (visiteur TypeChecking) ainsi que les tests (intégrés à la construction du compilateur et qui « passent »). Attention : vérifier aussi les affichages !
  3. ❑ Réaliser le contrôle de type du Projet MiniJAVA, c'est-à-dire pour avoir aussi des tableaux.
  4. ❑ Indiquer le statut des tâches pour cette phase dans le fichier binome_note_travail_sur_le_projet.md

Génération de la Forme Intermédiaire

Traduction de l'AST vers la Représentation Intermédiaire


On écrit un traducteur de l'AST vers la Représentation Intermédiaire en utilisant l'approche « code à 3 adresses » (cf. section 8 du Mémento MiniJAVA). 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 les calculs et traductions qui suivent :

  • 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 jalons de la syntaxe MiniJAVA et les méthodes de tests de la classe de tests test.SuccessfulMilestonesTest ou/et les exemples src/test/resources/Jalons/Test10*.java.

En guise d'aide, voici les exemples de programmes en représentation intermédiaire sans les optimisations :

Jalon Fichier de test Exemple de résultat RI
1 ./src/test/resources/Jalons/Test101.txt IR.101.out
2 ./src/test/resources/Jalons/Test102.txt IR.102.out
3 ./src/test/resources/Jalons/Test103.txt IR.103.out
4 ./src/test/resources/Jalons/Test104.txt IR.104.out
5 ./src/test/resources/Jalons/Test105.txt IR.105.out
6 ./src/test/resources/Jalons/Test106.txt IR.106.out
7 ./src/test/resources/Jalons/Test107.txt IR.107.out
9 ./src/test/resources/Jalons/Test109.txt IR.109.out
(avec certaines optimisations ;-) )

Attention aux potentiels « ... » dans le code inséré !

... /////////////////// Visit //////////////////// @Override public void visit(final KlassMain n) { final String mainMeth = "main"; add(new QLabel(newLabel(mainMeth))); String saved = this.currentMethod; this.currentMethod = mainMeth; defaultVisit(n); this.currentMethod = saved; add(new QParam(newConst(0))); add(new QCallStatic(newLabel("_exit"), newConst(1))); } @Override public void visit(final Method n) { add(new QLabelMeth(newLabel(n.methodId().name()), newConst(1 + n.fargs().nbChildren()))); String saved = this.currentMethod; this.currentMethod = n.methodId().name(); defaultVisit(n); this.currentMethod = saved; // add(new QReturn(getVar(n.returnExp()))); } // Expressions @Override public void visit(final ExprOpBin n) { defaultVisit(n); setVar(n, newTemp()); add(new QAssign(n.op(), getVar(n.expr1()), getVar(n.expr2()), getVar(n))); } @Override public void visit(final ExprOpUn n) { defaultVisit(n); setVar(n, newTemp()); add(new QAssignUnary(n.op(), getVar(n.expr()), getVar(n))); } @Override public void visit(final ExprLiteralInt n) { setVar(n, newConst(n.value())); } @Override public void visit(final ExprLiteralBool n) { setVar(n, newConst(Boolean.TRUE.equals(n.value()) ? 1 : 0)); } @Override public void visit(final ExprCall n) { defaultVisit(n); add(new QParam(getVar(n.receiver()))); // this for (AstNode f : n.args()) { add(new QParam(getVar(f))); } setVar(n, newTemp()); add(new QCall(newLabel(n.methodId().name()), newConst(n.args().nbChildren() + 1), getVar(n))); } @Override public void visit(final ExprIdent n) { setVar(n, lookupVar(n.varId().name(), n)); } @Override public void visit(final ExprNew n) { defaultVisit(n); setVar(n, newTemp()); add(new QNew(newLabel(n.klassId().name()), getVar(n))); } // Instructions @Override public void visit(final StmtPrint n) { defaultVisit(n); add(new QParam(getVar(n.expr()))); add(new QCallStatic(newLabel("_println"), newConst(1))); } @Override public void visit(final StmtAssign n) { defaultVisit(n); // setVar(n, lookupVar(n.varId().name(), n)); // non nécessaire add(new QCopy(getVar(n.value()), lookupVar(n.varId().name(), n))); } @Override public void visit(final StmtBlock n) { defaultVisit(n); } @Override public void visit(final StmtIf n) { final IRLabel l1 = newLabel(); final IRLabel l2 = newLabel(); n.test().accept(this); add(new QJumpCond(l1, getVar(n.test()))); n.ifTrue().accept(this); add(new QJump(l2)); add(new QLabel(l1)); n.ifFalse().accept(this); add(new QLabel(l2)); } @Override public void visit(final StmtWhile n) { final IRLabel l1 = newLabel(); final IRLabel l2 = newLabel(); add(new QLabel(l1)); n.test().accept(this); add(new QJumpCond(l2, getVar(n.test()))); n.body().accept(this); add(new QJump(l1)); add(new QLabel(l2)); } } ...

Quelques questions sur la génération de la forme intermédiaire


Dans le fichier binome_note_travail_sur_le_projet.md, copiez les énoncés et répondez aux questions ; pensez à fournir quelques explications. Dans un programme MiniJAVA :

  1. pour traduire les nœuds de l'AST de type ExprNew, que fait-on de l'appel au constructeur ?
  2. quel est le rôle de l'attribut currentMethod ? comment est-il manipulé ?
  3. que fait-on du problème du dangling else ? comment le traduit-on ?
  4. quel usage fait-on des étiquettes/labels ? pour quels types de nœud de l'AST sont-elles utilisées et pourquoi ? (on tiendra compte des bonus si on les a fait)

Génération de la Forme Intermédiaire : On ajoute les tableaux


Les éléments pour passer du Jalon 7 au Jalon 9 sont disponibles à la fin de cette page dans la section intitulée « Projet MiniJAVA en autonomie : On ajoute les tableaux ».

(Bonus 2) 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.

Ce bonus > plus particulièrement avec les fichiers Jalons/Test102.txt et Running/Test201.txt.

(Bonus 3) 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 deux 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é qu'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

Faire le même travail sur l'opérateur unaire "NOT" et le traduire sans utiliser de QAssignUnary.

Ce bonus > plus particulièrement avec le fichiers Jalons/Test106.txt et Running/Test202.txt.

Résumé des actions À FAIRE dans cette phase

  1. ❑ Répondre aux questions posées dans le fichier binome_note_travail_sur_le_projet.md
  2. ❑ Réaliser la génération de la forme intermédiaire (visiteur Intermediate) ainsi que les tests (intégrés à la construction du compilateur et qui « passent »). Attention : vérifier aussi les affichages !
  3. ❑ Réaliser la génération de la forme intermédiaire du Projet MiniJAVA, c'est-à-dire pour avoir aussi des tableaux.
  4. ❑ Indiquer le statut des tâches pour cette phase dans le fichier binome_note_travail_sur_le_projet.md

Génération de l'assembleur MIPS

Préliminaire important


Dans toute cette phase, qui conclue les travaux sur le compilateur MiniJAVA, l'enjeu est la validation du fonctionnement du compilateur sur les exemples du dossier ./src/test/resources/Jalons/*. Ces exemples sont utilisés dans la classe de test JUnit test.SuccessfulMilestonesTest.

On rappelle cette demande importante : garder les fichiers Jalons/Test10?.txt tels quels, et créer d'autres fichiers pour d'autres tests intermédiaires.

En guise d'aide, voici les exemples de traduction MIPS sans les optimisatons :

Jalon Fichier de test Exemple de résultat MIPS
1 ./src/test/resources/Jalons/Test101.txt Test101.mips
2 ./src/test/resources/Jalons/Test102.txt Test102.mips
3 ./src/test/resources/Jalons/Test103.txt Test103.mips
4 ./src/test/resources/Jalons/Test104.txt Test104.mips
5 ./src/test/resources/Jalons/Test105.txt Test105.mips
6 ./src/test/resources/Jalons/Test106.txt Test106.mips
7 ./src/test/resources/Jalons/Test107.txt Test107.mips
9 ./src/test/resources/Jalons/Test109.txt Test109.mips

Prise en main


La génération de l'assembleur est réalisée par la classe codegen.CodeGen qui utilise les classes des paquetages phase.e_codegen et phase.e_codegen.access :

  • la classe Reg fournit une énumération des registres MIPS pour les autres classes du paquetage ;
  • 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 « System.out.println(42); » (Test101) ;
  • la classe Allocator et le paquetage phase.e_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 phase.e_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 phase.e_codegen.access.Access* ;
  • la gestion des attributs d'instance des classes : 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.

Quelques remarques supplémentaires importantes :

  • MiniJAVA ne contient pas l'accès explicite aux champs d'un objet autrement que sur this. Dans la grammaire, le seul usage du token DOT est l'appel d'une méthode. Les attributs d'un objet ne sont accédés que sur la variable this qui est toujours dans le registre $a0 ;
  • la taille d'un objet de classe est définie en prenant la somme des attributs de la classe et de ces ancêtres. De même pour le calcul des offsets permettant d'accéder aux champs ;
  • les attributs d'objet sont toujours accédés sous la forme offset($a0) ;
  • seul la méthode main() contient des variables globales (temporaires). Elles sont référencées par offset($gp) ;
  • les variables locales (temporaires ou du programme) sont enregistrées dans le cadre d'appel ;
  • les tailles des cadres d'appel (framesize) sont calculées à partir d'une partie fixe (sauvegarde des registres) et incrémentées pour chaque déclaration de variable locale ;
  • les variables locales (= presque toutes les variables) sont accédées par offset($fp) (convention du cadre d'appel) ;
  • pour simplifier l'écriture de notre compilateur, on choisit de ne pas allouer les paramètres d'appels 1, 2 et 3 dans les registres $a_i (pour rappel, $a_0 est utilisé pour this), mais de les allouer comme de vraies variables locales dans le cadre d'appel. Il faudra donc, qu'à l'appel d'une fonction, initialiser ces variables locales avec les valeurs des registres. Ce choix est un choix d'implémentation de l'appelée qui ne change rien à la convention d'appel ;
    • N.B. sur ce choix : il est sous-optimal car il ne profite pas des allocations dans un registre encouragées par la convention d'appel. Mais, en contre-partie, il évite des risques d'écrasement quand on doit faire un appel et remplacer, dans les registres $a_i, les paramètres d'appel de l'appelante par ceux de l'appelée.
      Par exemple, une traduction un peu rapide de f(int toto, int tutu) { this.f(tutu,toto) } sous une forme move $a1, $a2 ; move $a2, $a1; jal f est clairement fausse ;
  • la seule allocation de variable dans un registre est celle de this dans le registre $a_0. Un compilateur performant réaliserait ici un travail important pour une allocation optimale dans les registres $t_i$, $s_i$, etc. des variables locales ou temporaires de notre forme intermédiaire. (cf. Algorithme de coloration de graphe).

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 !!!

Commencer par l'analyse de la structure de la classe ToMips : « Visiteur IR », Helpers, gestion des appels « spéciaux  (_system_*), etc. Noter par exemple ce qui suit :

  • l'accès aux variables IR (utilisation de phase.e_codegen.access) est réalisé avec les méthodes regLoad() et 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 ArrayList<IRvariable> params, qui est remplie par les instructions QParam, et utilisée, puis réinitialisée dans les instructions QCall et QCallStatic ;
  • ...

Ensuite, procéder par étapes :

  • traduction de l'instruction QAssign(+,*) comme suit :
    • charger les 2 arguments de QAssign dans $v0 et $v1 ;
    • effectuer l'opération avec le résultat dans $v0 
    • enregistrer $v0 dans le résultat de QAssign ;
  • traduction de l'instruction QNew. Appliquer ici une convention d'appel simplifié comme suit :
    • 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.
  • construction de la convention d'appel : QLabelMethod et QReturn pour l'appelée, QCall et QParam pour l'appelante comme suit :
    • QLabelMethod :
      • écrire le label de fonction ;
      • positionner le cadre d'appel (framepointer) $fp = $sp ;
      • allouer le cadre d'appel, c'est-à-dire avancer la pile de la taille du cadre d'appel Allocator.frameSize() ;
      • sauvegarder les registres adéquats ($ra, $s0, $s1, etc.) ;
      • recopier les valeurs des arguments $a1, $a2, $a3 dans les trois premières 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, etc.) ;
      • charger les (maximum quatre) 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*, etc.
    Aide-toy, le Ciel t’aidera. — (Jean de la Fontaine, Le Chartier embourbé, dans Fables, livre sixième, 1692-94)
Et un compilateur qui fonctionne !

Attention aux potentiels « ... » dans le code inséré !

... @Override public void visit(final QJump q) { mw.jump(q.arg1().name()); } @Override public void visit(final QJumpCond q) { regLoad(MIPSRegister.V0, q.arg2()); mw.jumpIfNot(MIPSRegister.V0, q.arg1().name()); } @Override public void visit(final QCopy q) { // optimisation 1 if (this.allocator.access(q.arg1()) instanceof AccessReg regArg1) { regStore(regArg1.getRegister(), q.result()); return; } // optimisation 2 if (this.allocator.access(q.result()) instanceof AccessReg regResult) { regLoad(regResult.getRegister(), q.arg1()); return; } // cas général regLoad(MIPSRegister.V0, q.arg1()); regStore(MIPSRegister.V0, q.result()); } @Override public void visit(final QNew q) { final String klassName = q.arg1().name(); final Integer size = this.allocator.classSize(klassName); if (size == null) { throw new compil.util.CompilerException("ToMips.QNew : unknown size for class " + klassName); } if (size < 0) { throw new compil.util.CompilerException("ToMips.QNew : negative size malloc for class " + klassName); } push(MIPSRegister.A0); mw.load(MIPSRegister.A0, size); mw.jumpIn("_new_object"); regStore(MIPSRegister.V0, q.result()); pull(MIPSRegister.A0); } @Override public void visit(final QAssign q) { regLoad(MIPSRegister.V0, q.arg1()); regLoad(MIPSRegister.V1, q.arg2()); switch (q.op()) { case PLUS -> mw.plus(MIPSRegister.V0, MIPSRegister.V1); case MINUS -> mw.moins(MIPSRegister.V0, MIPSRegister.V1); case TIMES -> mw.fois(MIPSRegister.V0, MIPSRegister.V1); case AND -> mw.et(MIPSRegister.V0, MIPSRegister.V1); case LESS -> mw.inferieur(MIPSRegister.V0, MIPSRegister.V1); default -> throw new compil.util.CompilerException("Invalid binary Operator " + q.op()); } regStore(MIPSRegister.V0, q.result()); } @Override public void visit(final QAssignUnary q) { regLoad(MIPSRegister.V0, q.arg1()); if (q.op() == compil.EnumOper.NOT) { mw.not(MIPSRegister.V0); } else { throw new compil.util.CompilerException("Invalid unary Operator " + q.op()); } regStore(MIPSRegister.V0, q.result()); } @Override public void visit(final QLabelMeth q) { final String function = q.arg1().name(); mw.label(function); mw.com("--création de la trame dans l'appelé"); mw.com("positionne la trame $fp (et sauvegarde $sp)"); mw.move(MIPSRegister.FP, MIPSRegister.SP); // positionne la trame $fp (et sauvegarde $sp) mw.com("alloue la trame : " + MIPSRegister.SP + ", " + -allocator.frameSize(function)); mw.plus(MIPSRegister.SP, -allocator.frameSize(function)); // alloue la trame calleeSave(); // A1, A2, A3 recopiés comme variables locales final int local0 = -allocator.frameSizeMin() - SIZEOF; mw.com("recopie de A1, A2, A3 comme variables locales : " + local0); final int nbArg = q.arg2().value(); if (nbArg > 1) { mw.storeOffset(MIPSRegister.A1, local0, MIPSRegister.FP); } if (nbArg > 2) { mw.storeOffset(MIPSRegister.A2, local0 - SIZEOF, MIPSRegister.FP); } if (nbArg > 3) { mw.storeOffset(MIPSRegister.A3, local0 - 2 * SIZEOF, MIPSRegister.FP); } // NB : l'execution des 3 copies sans tester le nombre d'arguments // donne aussi un code correct // Initialisation des variables locales // on devrait le faire ici mais on a oublié !!! mw.com("--fin de la création de la trame dans l'appelé"); mw.com("--code de l'appelé " + q.arg1()); } @Override public void visit(final QReturn q) { calleeRestore(); regLoad(MIPSRegister.V0, q.arg1()); mw.com("--déallocation de la trame en restaurant SP à partir de FP"); mw.move(MIPSRegister.SP, MIPSRegister.FP); mw.com("--retour à l'appelant en utilisant RA qui vient d'être récupéré"); mw.jumpOut(); } @Override public void visit(final QCall q) { final String function = q.arg1().name(); final int nbArg = q.arg2().value(); // check nombre d'arguments if (nbArg != this.params.size()) { throw new compil.util.CompilerException("ToMips : Params error"); } // Sauvegarde registres non préservés par un appel de fonction callerSave(); // Passage des Arguments dans $A0-$A3 puis dans la pile // N.B. : Danger d'écrasement sur les registres $A_i : // si les valeurs des arguments de l'appelé dépendent // des arguments de l'appelant, il ne faut pas prendre // ces valeurs dans les registres $A_i mais dans la sauvegarde des $A_i // que l'on vient de faire sur la pile ! // Exemple : rec.f(tutu,titi) avec tutu = champs d'objet // this = $A0 // tutu == this.tutu == Offset_tutu ( $A0 ) // si on affecte d'abord $A0 (this=rec), on a perdu (rec.tutu??) !!! // Seul $A0 reste alloué dans un registre // les parametres $A1, $A2, $A3 sont obligatoirement // recopiées dans des variables locales allouées dans la frame. // => Seule contrainte contre l'écrasement : // l'argument A0 est chargé (et écrasé) le dernier. // arguments 1, 2 ,3 dans A1, A2, A3 mw.com("--arguments 1, 2, 3 dans A1, A2, A3"); for (int i = 1; i < NBARGS && i < nbArg; i++) { regLoad(AREGS[i], this.params.get(i)); } // argument 0 == this, obligatoirement le dernier, dans $A0 mw.com("--argument 0 == this, obligatoirement le dernier, dans $A0"); regLoad(MIPSRegister.A0, this.params.get(0)); // Jump mw.com("--saut dans la fonction " + function); mw.jumpIn(function); // restauration registre callerRestore(); // resultat dans V0 regStore(MIPSRegister.V0, q.result()); // clear des QParams this.params.clear(); } } ...

Quelques questions sur la génération de l'assembleur MIPS


Dans le fichier binome_note_travail_sur_le_projet.md, copiez les énoncés et répondez aux questions ; pensez à fournir quelques explications. Dans un programme MiniJAVA :

  1. Quels sont les instructions de la représentation intermédiaire (quadruplets) qui sont impliquées dans la réalisation de la convention d'appel ? Quelles sont les méthodes utilitaires de la classe ToMips que l'on peut enchaîner pour la mise en œuvre de la convention d'appel (de l'appel au retour, chez l'appelante et chez l'appelée) ? Indiquer la séquence.
  2. Dans une méthode d'instance, comment accède-t-on à un attribut de l'instance ? Via quel registre ?
  3. Quelles sont les variables globales dans un programme MiniJAVA ? Où sont-elles en mémoire ?

Génération de l'assembleur MIPS : On ajoute les tableaux


Les éléments pour passer du Jalon 7 au Jalon 9 sont disponibles à la fin de cette page dans la section intitulée « Projet MiniJAVA en autonomie : On ajoute les tableaux ».

(Bonus 4) Manque d'arguments


Supprimer la contrainte sur le nombre d'arguments des méthodes. La classe phase.e_codegen.Allocator implémente déjà la convention d'appel et définit l'accès aux arguments 4 à N (0($fp), 4($fp), etc.), c'est-à-dire dans la pile avant la frame de l'appelée. Il faut uniquement modifier les séquences d'appel et de sortie au niveau de l'appelante.

Pour ce faire, modifier le traitement de l'instruction QCall pour accepter un nombre quelconque d'arguments dans le langage MiniJAVA.

Bien empiler (modification de $sp) les arguments supplémentaires avant de créer le cadre d'appel (frame) et de dépiler à la fin.

Vérifier que le compilateur fonctionne encore. Dans la classe de tests JUnit test_other.RunningTest :

  • src/test/resources/Running/Test204.txt : appel de fonction avec moins de 4 arguments ;
  • src/test/resources/Running/Test205.txt : appel de fonction avec plus de 4 arguments ;
  • src/test/resources/Running/AckermannFunction.txt : appel de fonction récursive « gourmande ».

Résumé des actions À FAIRE dans cette phase

  1. ❑ Répondre aux questions posées dans le fichier binome_note_travail_sur_le_projet.md
  2. ❑ Réaliser la génération de l'assembleur MIPS (visiteur IRvisitorDefault) ainsi que les tests (intégrés à la construction du compilateur et qui « passent »). Attention : vérifier aussi les affichages !
  3. ❑ Réaliser la génération de l'assembleur MIPS du Projet MiniJAVA, c'est-à-dire pour avoir aussi des tableaux.
  4. ❑ Indiquer le statut des tâches pour cette phase dans le fichier binome_note_travail_sur_le_projet.md

Projet MiniJAVA en autonomie : On ajoute les tableaux

Les exercices précédents de la page 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, selon l'état d'avancement dans le projet, que, dans la phase courante, le compilateur est raisonnablement fonctionnel : par exemple, dans la phase d'analyse lexicale et d'analyse syntaxique, « des tests passent »
  • Faire un commit/backup/mail_a_1_ami pour ne pas casser le « compilateur qui marche ».
  • Remarquez que les tests JUnit existants, que vous avez écrits ou que vous avez enabled (en retirant l'annotation @Disabled), agissent comme des tests de non-régression. Ils font partie du processus de construction du compilateur avec Maven. Notez cependant que les tests de la phase de génération du code MIPS avec ensuite l'exécution du programme généré avec Mars « se contentent » de tester la levée d'une exception ; autrement dit, les résultats des exécutions Mars ne sont pas testés.

Analyse lexicale et syntaxique


A priori, deux token et cinq règles de grammaire sont à ajouter dans les spécifications JFlex et CUP.

Par ailleurs, quatre nouveaux nœuds sont à utiliser dans l'AST : phase.b_syntax.ast.*Array*.

Ne pas oublier non plus 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 types de nœud de l'AST :

  • ExprArrayLength : contrôle du type de l'expression dans expr.length ;
  • ExprArrayLookup : contrôle du type des deux expressions dans exprArray[exprIndice] ;
  • ExprArrayNew : contrôle du type INT pour la taille du tableau ;
  • StmtArrayAssign : contrôle du type INT pour l'indice de la case du tableau accédée et contrôle du type « tableau de XXX » pour la variable correspondant au tableau.

Représentation Intermédiaire


Les quatre nouveaux types de nœuds de l'AST correspondent à quatre nouvelles instructions de la Représentation Intermédiaire : QLength, QAssignArrayFrom, QNewArray, et QAssignArrayTo. La traduction des trois premiers type de nœuds inclue la création d'une nouvelle variable temporaire et celle du dernier recherche la variable correspondant au tableau accédé.

Voici un exemple de résultat : IR.109.out.

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

Validation


Valider le bon fonctionnement du compilateur.

Utiliser les Exemples */Test*9.txt 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 !)

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


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.

Voici quelques éléments :

  • Utiliser l'instruction MIPS : sltu $t0, $t1, $t2 (?!?). Cf. méthode phase.e_codegen.MipsWriter::inRange.
  • Gérer une sortie en erreur en utilisant la fonction _system_exit(errorCode) du Runtime MIPS (errorCode=111 ou 42 ou...).

(Bonus 6) 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 !!!

(Bonus 7) Tableaux d'objets


Ajouter au langage MiniJAVA, les tableaux d'objets.

Rendu et Barème du projet MiniJAVA

En plus des fonctionnalités dont la solution est fournie, le compilateur MiniJAVA intègre :
  • la gestion des tableaux d'entiers ;
  • des boni :
    • (Bonus 1) Phases 2-3 : La fonction sémantique Undefined/Unused pour les variables, les classes et les méthodes ;
    • (Bonus 2) Phase 4 : Propagation de constantes ;
    • (Bonus 3) Phase 4 : Optimisation d'expression booléenne ;
    • (Bonus 4) Phase 5 : Manque d'arguments ;
    • (Bonus 5) Projet MiniJAVA avec les tableaux : Contrôle de borne de tableau ;
    • (Bonus 6) Projet MiniJAVA avec les tableaux : Tableaux de booléens ;
    • (Bonus 7) Projet MiniJAVA avec les tableaux : Tableau d'objets, null, == (opérateur surchargé), division euclidienne (/,%), System.exit(i), etc.

Consignes pour la livraison :

  • renseigner le fichier MarkDown binome_note_travail_sur_le_projet.md : le fichier est rempli avec les noms des auteurs et avec les réponses aux questions, et est à jour de ce qui a été fait. Entre autres, en lisant ce fichier, pour chaque phase, on sait ce qui a été fait et on trouve si nécessaire des commentaires utiles pour le correcteur ;
  • faire le ménage avant de construire l'archive : retirer les répertoires et les fichiers *~, *backup, .DS_Store, etc. ;
  • utiliser le script livraison.sh pour la construction de l'archive à déposer dans Moodle.
Barème indicatif :
  • 3 points : Sources d'origine et codes des phases 1 à 5 (éléments fournis), y compris les tests correspondants qui passent :
    • une méthode de test annotée @Disabled est considérée comme un test qui ne passe pas ;
    • hormis un test : laisser de côté le test test_other.RunningTest.testFilePeanoPlusUTF8Матрёшка.
  • 3 points : Fichier binome_note_travail_sur_le_projet.md qui (1) donne des réponses pertinentes aux questions dans les différentes phases et (2) renseigne correctement sur ce qui est fait, fonctionne, ne fonctionne pas, etc.
  • 9 Points : Tableaux d'entiers
  • 1 à 2 points par bonus
  • Total : 15 sans bonus, 20 avec 4 boni, 24 avec les 7 boni

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