Le principal role du parser est de reconnaître les ordres contextuels décrits par les jetons spécifiés par un ensemble de règles d'analyse.
Lorsqu'on utilise ANTLR4 pour créer un parser, il est important de se rappeler qu'ANTLR4 suppose que les noms de grammaire de l'analyseur commencent par une lettre minuscule, par opposition à un nom de grammaire de lexer. Cela permet de combiner les deux types de règles dans un même fichier de grammaire.
A partir de l'ensemble de règles définies dans la grammaire, le parser réalise la correspondance des jetons d'entrée et génère comme sortie l'AST (Abstract Syntax Tree).
L'AST est une représentation arborescente de la structure syntaxique abstraite du code source écrit dans un langage de programmation. Chaque nœud de l'arborescence désigne un jeton identifié, reconnu comme étant dans un ordre correct pour le langage défini dans la grammaire.
En ANTLR, le Visitor représente une fonctionnalité qui permet de parcourir l'arbre d'analyse (parse tree) généré automatiquement, son principal role étant de visiter et traiter les noeuds du parse tree, pour construire l'arbre d'analyse (AST).
Ainsi, ANTLR4 peut parcourir l'AST progressivement, en appelant notre code aux nœuds d'intérêt.
Les caractéristiques du Visitor sont:
visit()
sur les enfants d'un nœud signifie que ces sous-arbres ne sont pas visités
Afin d'obtenir des Visitors plus précis des arbres d'analyse, on peut étiquetter les alternatives des règles en utilisant l'opérateur #
. Il est obligatoire soit d'associer des étiquettes pour toutes les sous-regles, soit pour aucune.
grammar Alf; start : declaration #declarationRule ; declaration : type VARIABLE EQ value #variableDeclaration ; type : INT #typeInt | FLOAT #typeFloat | STRING #typeString ; value : INT_NUMBER #valueInt | FLOAT_NUMBER #valueFloat | STRING_TEXT #valueString | VARIABLE #valueVariable ; WS : (' ') -> skip; NEWLINE : ([\r\n]+) -> skip; VARIABLE : ('_'[a-zA-Z0-9]+); INT : 'int'; FLOAT : 'float'; STRING : 'string'; EQ : '='; SEMICOLON : ';'; INT_NUMBER : ([0-9]+); FLOAT_NUMBER: ([0-9]+'.'[0-9]+); STRING_TEXT : ('"'~["]+'"'|'\''~[']+'\'');
Si on va étiquetter les alternatives, dans le fichier AlfParser.ts
on obtiendra des classes spécifiques pour chaque type de sous-regle et dans le fichier AlfVisitor.ts
les interfaces exportées incluront des fonctions dépendant du contexte pour chaque étiquette définie:
public interface AlfVisitor<T> extends ParseTreeVisitor<T> { T visitDeclarationRule(AlfParser.DeclarationRuleContext ctx); T visitVariableDeclaration(AlfParser.VariableDeclarationContext ctx); T visitTypeInt(AlfParser.TypeIntContext ctx); T visitTypeFloat(AlfParser.TypeFloatContext ctx); T visitTypeString(AlfParser.TypeStringContext ctx); T visitValueInt(AlfParser.ValueIntContext ctx); T visitValueFloat(AlfParser.ValueFloatContext ctx); T visitValueString(AlfParser.ValueStringContext ctx); T visitValueVariable(AlfParser.ValueVariableContext ctx); }
Ces fonctions seront écrasées (overwritten) plus tard afin de visiter les noeuds.
Pour générer l'AST a l'aide du Visitor, on va créer des classes représentatives pour chaque noeud.
La classe abstraite ASTNode
décrit de façon générale un noeud de l'arbre, contenant aussi la fonction toJSON(), dont le but principal est de retourner, pour chaque type de noeud, un objet contenant, outre les propriétés spécifiques, le nom de la classe en tant quid du noeud.
Chaque type de noeud est représenté comme une classe enfant qui étend la classe ASTNode
et qui contient dans le constructor ses paramètres spécifiques, comme dans l'exemple suivant.
// On fait une classe abstraite ASTNode qui sera hérité par toutes les noeuds // ASTNode.java abstract class ASTNode { ASTNode(){} } // Puis, tous les autres noeuds qui sont nommees dans notre grammaire // DeclarationNode.java public class DeclarationNode extends ASTNode{ String id = "declaration"; String variableType; String variable; ASTNode value; DeclarationNode(String variableType, String variable, ASTNode value) { super(); this.variableType = variableType; this.variable = variable; this.value = value; } } // TypeNode.java public class TypeNode extends ASTNode{ String id = "value"; String typeName; TypeNode(String typeName) { super(); this.typeName = typeName; } } // ValueNode.java public class ValueNode extends ASTNode{ String id = "value"; public Object value; ValueNode(Object value) { super(); this.value = value; } }
Pour visiter les composants et générer l'arbre, il faut surcharger les méthodes du visiteur de base de la façon suivante: on fait notre classe visiteur, MyAlfVisitor, qui va implementer l'interface du visiteur genree par la grammaire AlfVisitor. L'action de visiter un noeud représente parcourir l'arbre d'analyse créé par le parser, la création d'un objet du type de règle mentionné dans l'AST et sa transmission à la notre classe visiteur.
public class MyAlfVisitor extends AbstractParseTreeVisitor<ASTNode> implements AlfVisitor<ASTNode> { @Override public ASTNode visitDeclarationRule(AlfParser.DeclarationRuleContext ctx) { return (DeclarationNode) this.visit(ctx.declaration()); } @Override public ASTNode visitVariableDeclaration(AlfParser.VariableDeclarationContext ctx) { return new DeclarationNode( ((TypeNode) this.visit(ctx.type())).typeName, ctx.VARIABLE().getText(), String.valueOf(((ValueNode) this.visit(ctx.value())).value) ); } @Override public ASTNode visitTypeInt(AlfParser.TypeIntContext ctx) { return new TypeNode( ctx.INT().getText() ); } @Override public ASTNode visitTypeFloat(AlfParser.TypeFloatContext ctx) { return new TypeNode( ctx.FLOAT().getText() ); } @Override public ASTNode visitTypeString(AlfParser.TypeStringContext ctx) { return new TypeNode( ctx.STRING().getText() ); } @Override public ASTNode visitValueInt(AlfParser.ValueIntContext ctx) { return new ValueNode( Integer.parseInt(ctx.INT_NUMBER().getText()) ); } @Override public ASTNode visitValueFloat(AlfParser.ValueFloatContext ctx) { return new ValueNode( Float.parseFloat(ctx.FLOAT_NUMBER().getText()) ); } @Override public ASTNode visitValueString(AlfParser.ValueStringContext ctx) { return new ValueNode( ctx.STRING_TEXT().getText() ); } @Override public ASTNode visitValueVariable(AlfParser.ValueVariableContext ctx) { return new ValueNode( ctx.VARIABLE().getText() ); } }
Pour voir le résultat de l'arbre généré, on utilise le format de notation JSON. Pour utiliser JSON pour afficher des classes Java, on doit telecharger et importer le paquet GSON, et de l'ajouter dans le projet Intellij avec File > Project Structure > Modules
. La page de modules doit ressembler à ça:
Et le code pour afficher le contenu JSON dans un fichier output.json
:
// Une methode toJSON qui utilise les fonctions du paquet GSON pour interpreter un noeud AST: static String toJSON(ASTNode visitor) { GsonBuilder builder = new GsonBuilder(); builder.setPrettyPrinting(); Gson gson = builder.create(); return gson.toJson(visitor); } // Appeller dans Main.java: writeToFile("files\\output.json", toJSON(visitor.visit(tree)));
Pour un fichier sample.txt
contenant le texte int _var = 5;
alf.g4
), vous devez generer de nouveau les fichiers de ANTLR, avec clic-droit sur le fichier de grammaire > Generate ANTLR Recognizer
;
et une ou plusieurs lignes vides. Ajoutez les classes et les méthodes nécessaires pour pouvoir visiter les noeuds. Dans l'AST, chaque instruction doit être ajoutée dans la liste statements du noeud principal. Testez le programme pour les instructions suivantes: (3p) float _var1 = 7.5; string _var2 = 'alf';
int _var1 = 1; 5*(2+4)/7;
float _var1 = 5*(2+4)/7;