Théorie des Langages AntLR - Générateur d analyseurs Claude Moulin Université de Technologie de Compiègne NF11
Sommaire 1 Introduction 2 Evaluation d une expression 3 Expressions arithmétiques
AntLR ANTLR : ANother Tool for Language Recognition URL : http://www.antlr.org. Auteur : Terence Parr (Université de San Francisco) Il fournit un parser qui interagit avec le lexer, fournit un rapport d erreur assez explicite et construit des arbres syntaxiques.
Objectif AntLR est un outil de génération automatique d analyseur permettant de reconnaître les phrases d un programme écrit dans un langage donné. L analyseur est généré à partir d un fichier grammaire contenant les règles définissant le langage. Code généré : classes en Java, C# ou C++, Python, etc. AntLR définit un Méta Langage utilisé pour écrire le fichier grammaire.
Mise en œuvre d AntLR Sommaire 1 Introduction Mise en œuvre d AntLR Analyses Principe de fonctionnement Grammaire v4 2 Evaluation d une expression Utilisation du paramètre de retour 3 Expressions arithmétiques Utilisation d une Map Potentiel d ANTLR
Mise en œuvre d AntLR Génération du code (Windows) - On crée un fichier batch qui appelle le générateur d AntLR et permet de générer le code à partir d un fichier grammaire g4 (antlr-generat.bat). @echo off set CLASSPATH=lib/antlr-4.6-complete.jar; java org.antlr.v4.tool -visitor -o src/logoparsing grammars/logo.g4 pause Syntaxe : java org.antlr.v4.tool -visitor -o <output-folder> <grammaire.g4> ;
Mise en œuvre d AntLR Synchronisation avec Eclipse Synchroniser le code généré avec l environnement de développement Eclipse. Créer un projet Eclipse et un package pour les classes créées par le fichier batch ; Mettre le fichier batch à la racine du projet Créer la grammaire dans un répertoire (grammars/logo.g4) ; Utiliser l option -o pour indiquer le répertoire où les fichiers sont générés. Utiliser l option -visitor pour générer aussi les classes pour le design pattern Visitor.
Analyses Sommaire 1 Introduction Mise en œuvre d AntLR Analyses Principe de fonctionnement Grammaire v4 2 Evaluation d une expression Utilisation du paramètre de retour 3 Expressions arithmétiques Utilisation d une Map Potentiel d ANTLR
Analyses Analyses Analyse lexicale Flux de caractères Flux de tokens Analyse syntaxique Flux de tokens Interprétation Flux de tokens AST Traitement à partir de l arbre AST AST Interprétation
Analyses Analyse Lexicale L analyseur lexical (lexer) est l outil qui permet de découper un flux de caractères en un flux de mots du langage (tokens). Flux de caractères : a i r e := b a s e x h a u t e u r Lexer t 1 (aire) t 2 ( :=) t 3 (base) t 4 ( ) t 5 (hauteur)
Analyses Analyse Syntaxique L analyseur syntaxique (parser) vérifie que l ensemble des mots issus de l analyse lexicale (les tokens) forme une phrase syntaxiquement correcte. Il n y a pas de garantie concernant la sémantique de cette phrase. Flux de caractères : a i r e := b a s e x h a u t e u r Lexer t 1 (aire) t 2 ( :=) t 3 (base) t 4( ) t 5 (hauteur) Parser Arbre syntaxique
Analyses Génération
Analyses Exemple Grammaire permettant d écrire des expressions arithmétiques du type : 10-3 * 5 (20-10) / 4 Symboles opératoires : +, -, *, /, ^ ; Les opérandes sont des nombres entiers ; Il est possible d utiliser des parenthèses ; Les espaces et tabulations sont ignorés mais servent de séparateurs.
Analyses Fichier grammaire : Expr.g4 grammar Expr; @header { package expression; INT : 0.. 9 + ; WS : [ \t\r\n]+ -> skip ; expr: sumexpr ; sumexpr : multexpr (( + - ) multexpr)* ; multexpr: powexpr (( * / ) powexpr)* ; powexpr : atom ( ^ atom)* atom: INT ( expr ) ;
Principe de fonctionnement Sommaire 1 Introduction Mise en œuvre d AntLR Analyses Principe de fonctionnement Grammaire v4 2 Evaluation d une expression Utilisation du paramètre de retour 3 Expressions arithmétiques Utilisation d une Map Potentiel d ANTLR
Principe de fonctionnement Règles Les catégories lexicale (lexer) sont en majuscule. Les catégories syntaxiques (variables) du parser sont en minuscule. expr est considéré comme l axiome de la grammaire. AntLr déclare automatiquement les tokens.
Principe de fonctionnement Classes générées (exemple Java) 1 Lexer : ExprLexer.java 2 Parser : ExprParser.java 3 Listes de tokens : Expr.tokens ; ExprLexer.tokens 4 Listeners : ExprListener.java (interface) ExprBaseListener.java (classe implémentant à vide l interface) 5 Visitors : ExprVisitor.java (interface) ExprBaseVisitor.java (classe implémentant à vide l interface)
Principe de fonctionnement Parser public class ExprParser extends Parser { public ExprParser(TokenStream input) {... public final ExprContext expr() throws RecognitionException {... public static class ExprContext extends ParserRuleContext {... Pour chaque règle syntaxique une méthode reprenant le nom de la règle et une classe contexte sont créées pour l analyse d une phrase.
Principe de fonctionnement Utilisation du parser Un parser ANTLR construit un arbre syntaxique à partir du programme donné en entrée. Il est nécessaire de parcourir cet arbre pour effectuer des actions. Traitements : Simple : un seul parcours de l arbre en profondeur d abord est nécessaire pour le traitement. On utilise un listener d événements. Evolué : le parcours de l arbre n est pas nécessairement complet ; une sous-arborescence peut être parcourue plusieurs fois. On utilise un visiteur d arbre.
Principe de fonctionnement Visitor ANTLR genère une interface visitor et une classe de base implémentant à vide l interface. Un visitor est un design pattern dont le but est d exécuter une opération sur les éléments d une structure (arbre) sans changer le type des éléments de la structure sur laquelle il opère. public class ExprBaseVisitor<T> extends ParseTreeVisitor<T> implements ExprVisitor<T> {... @Override public T visitexpr(exprparser.exprcontext ctx) { return visitchildren(ctx);...
Principe de fonctionnement Type du Visiteur ANTLR utilise un type paramètre pour la valeur de retour des méthodes du visiteur. Lors de la création de la classe visiteur il faut dériver la classe de base fournie par ANTLR avec le choix du type. Intéressant dans les cas simples lorsque toutes les méthodes doivent retourner le même type de valeurs. to implement. public class ExprEvalVisitor extends ExprBaseVisitor<Double> { @Override public Double visitexpr(exprcontext ctx) { return visit(ctx.sumexpr());...
Principe de fonctionnement Méthode statique minimale public static String program = "programs/expr.txt"; public static void run_mini() { try { // lexer InputStream is = new FileInputStream(program); ANTLRInputStream stream = new ANTLRInputStream(is); Expr1Lexer lexer = new Expr1Lexer(stream); // parser CommonTokenStream tokens = new CommonTokenStream(lexer); ExprParser parser = new ExprParser(tokens); ParseTree tree = parser.expr(); System.out.println(tree.toStringTree(parser)); catch(exception ex) { ex.printstacktrace();
Principe de fonctionnement Résultat pour : 5*(10-2) (expr (sumexpr (multexpr (powexpr (atom 5)) * (powexpr (atom ( (expr (sumexpr (multexpr (powexpr (atom 10))) - (multexpr (powexpr (atom 2))))) ))))))
Grammaire v4 Sommaire 1 Introduction Mise en œuvre d AntLR Analyses Principe de fonctionnement Grammaire v4 2 Evaluation d une expression Utilisation du paramètre de retour 3 Expressions arithmétiques Utilisation d une Map Potentiel d ANTLR
Grammaire v4 Métalangage du lexer Opérateurs concaténation alternative : au moins 1 : + 0 ou plus (Kleene) : * 0 ou 1 :? négation : caractère quelconque :. Parenthèses pour forcer l appel des opérateurs.
Grammaire v4 Facilités Fragment pour simplifier l écriture de certaines règles : fragment LET : A.. Z a.. z ; fragment DIGIT : 0.. 9 ; ID : LET (LET DIGIT)+ ; SPACE : ; Pour ignorer les espaces, tabulations, retours à la ligne dans l écriture d un programme : WS : [ \t\r\n]+ -> skip ;
Grammaire v4 Nommer chaque alternative expr : sumexpr ; sumexpr : multexpr (( + - ) multexpr)* ; multexpr : powexpr (( * / ) powexpr)* ; powexpr : atom ( ^ atom)* ; atom : INT #int ( expr ) #parent ; Il est nécessaire de donner un nom à chaque cas d une alternative pour générer une méthode adaptée du visiteur
Grammaire v4 Accès aux nœuds de l arbre Un parser ANTLR construit un arbre syntaxique à partir du programme donné en entrée. Les méthodes des listener et visitor créés par ANTLR ont un paramètre (ctx) dont le type est une classe décrivant le nœud de l arbre créé par le parser. Ce paramètre permet d accéder aux nœuds fils. Depuis visitexpr on a accès à ctx.sumexpr(). Plusieurs fils de même nom sont accessibles dans une liste : depuis visitmultexpr on a accès à ctx.powexpr(). Accès aux tokens par leur nom : depuis visitint on a accès à INT().
Grammaire v4 Forme générale d une grammaire Déclaration de la grammaire grammar <GrammarName> ; Section @header{... déclaration de package @header { package expression2; Règles lexicales (Majuscule) Règles syntaxiques (Minuscule)
Sommaire 1 Introduction 2 Evaluation d une expression 3 Expressions arithmétiques
Principe du visitor Définir la méthode de visite de chaque nœud de l arbre (règle de la grammaire). Elle peut simplement appeler la méthode de visite des fils du nœud Le paramètre contexte de chaque méthode donne accès aux éléments fils du nœud courant. Appeler la méthode de visite sur les nœuds fils pertinents. Type : le type de chaque méthode sera Double (division). public class ExprEvalVisitor extends ExprBaseVisitor<Double> {...
Utilisation du paramètre de retour Sommaire 1 Introduction Mise en œuvre d AntLR Analyses Principe de fonctionnement Grammaire v4 2 Evaluation d une expression Utilisation du paramètre de retour 3 Expressions arithmétiques Utilisation d une Map Potentiel d ANTLR
Utilisation du paramètre de retour Evaluation d un nombre Elle correspond à l alternative int. public Double visitint(intcontext ctx) { String inttext = ctx.int().gettext(); return Double.valueOf(intText);
Utilisation du paramètre de retour Evaluation d une expression Il suffit de visiter le nœud fils. public Double visitexpr(exprcontext ctx) { return visit(ctx.sumexpr());
Utilisation du paramètre de retour Evaluation d une parenthèse Il suffit de visiter le nœud expression fils. public Double visitparent(parentcontext ctx) { return visit(ctx.expr());
Utilisation du paramètre de retour Evaluation d une powexpr On doit visiter les nœuds atom fils. public Double visitpowexpr(powexprcontext ctx) { Double left = visit(ctx.atom(0)); Double right = visit(ctx.atom(l));... Comme un nœud atom est soit un nœud int soit un nœud parent, visit(ctx.atom(0)) appelle visitint ou visitparent suivant le cas.
Utilisation du paramètre de retour Evaluation d une expression somme Une somme peut avoir plusieurs opérandes. On dispose de la liste des nœuds fils opérandes ctx.multexpr() et de tous les nœuds fils. Les opérateur sont les fils de rang impair. public Double visitsumexpr(sumexprcontext ctx) { int l = 0; int nb = ctx.multexpr().size(); Double result = visit(ctx.multexpr(l)); l++; int oprow = 1; while (l < nb) { Double right = visit(ctx.multexpr(l)); result = ctx.getchild(oprow).gettext().equals("+")? result + right : result - right; l++; oprow +=2 ; return result;
Utilisation du paramètre de retour Evaluation d une expression produit
Utilisation du paramètre de retour Evaluation d une expression produit Méthode similaire public Double visitmultexpr(multexprcontext ctx) { int l = 0; int nb = ctx.powexpr().size(); Double left = visit(ctx.powexpr(l)); Double result = left; l++; int oprow = 1; while (l < nb) { Double right = visit(ctx.powexpr(l)); result = ctx.getchild(oprow).gettext().equals("*")? result * right : result / right; l++; oprow +=2 ; return result;
Utilisation du paramètre de retour Programme public static void run_default_visitor() { try { InputStream is = new FileInputStream("prg/exp.txt"); ANTLRInputStream stream = new ANTLRInputStream(is); Expr1Lexer lexer = new Expr1Lexer(stream); CommonTokenStream tokens = new CommonTokenStream(lexer); Expr1Parser parser = new Expr1Parser(tokens); ParseTree tree = parser.expr(); Expr1EvalVisitor visitor = new Expr1EvalVisitor(); Double result = visitor.visit(tree); System.out.println("Result = " + result); catch(exception ex) { ex.printstacktrace();
Utilisation du paramètre de retour Résultat Expression : 5 * 3 / 2 * (15-2) Résultat : 97.5 Avantage : le visiteur retourne le résultat Désavantage : toutes les méthodes doivent retourner le même type de valeurs.
Sommaire 1 Introduction 2 Evaluation d une expression 3 Expressions arithmétiques
Conditions Dans les langages de programmation, la grammaire doit permettre d écrire et d évaluer correctement des expressions aritmétiques : fixer la priorité des opérateurs assurer l associativité à gauche utiliser des parenthèses pour forcer certaines associations 100-30 + 20 doit être évalué à 90 100-30 * 2 doit être évalué à 40 100-300 / 30 * 5 doit être évalué à 50
Grammaire expr : sumexpr ; sumexpr : multexpr (( + - ) multexpr)* ; multexpr : powexpr (( * / ) powexpr)* ; powexpr : atom ( ^ atom)* ; atom : INT #int ( expr ) #parent ;
Analyse descendante Dans le cas de grammaire de type LL Les règles des opérateurs les moins prioritaires doivent être appelées avant lors des parsing Les opérateurs de même force doivent se retrouver dans les mêmes règles.
Exemple : 100-300 / 30 * 5
Utilisation d une Map Sommaire 1 Introduction Mise en œuvre d AntLR Analyses Principe de fonctionnement Grammaire v4 2 Evaluation d une expression Utilisation du paramètre de retour 3 Expressions arithmétiques Utilisation d une Map Potentiel d ANTLR
Utilisation d une Map Méthode ANTLR fournit une classe ParseTreeProperty pour une map d objets indexés par les nœuds de l arbre. Chaque méthode stocke dans la map la valeur qu elle calcule. Chaque méthode va chercher dans la map les valeurs dont elle a besoin. Chaque méthode retourne un entier qui indique si le calcul s est bien passé.
Utilisation d une Map Déclaration de la Map // Map ParseTreeProperty<Double> attributes = new ParseTreeProperty<Double>(); // Accesseurs void setvalue(parsetree node, Double value) { attributes.put(node, value); Double getvalue(parsetree node) { return attributes.get(node); public Double getresult() { return getvalue(tree);
Utilisation d une Map Evaluation d un nombre Elle correspond à l alternative int. atom : int parent INT #int / \ ( expr ) #parent INT ( expr ) ; public Integer visitint(intcontext ctx) { String inttext = ctx.int().gettext(); setvalue(ctx, Double.valueOf(intText)); return 0;
Utilisation d une Map Evaluation d une expression expr : sumexpr Il suffit de visiter le nœud fils. public Integer visitexpr(exprcontext ctx) { visit(ctx.sumexpr()); setvalue(ctx,getvalue(ctx.sumexpr())); return 0;
Utilisation d une Map Evaluation d une parenthèse Il suffit de visiter le nœud expression fils. public Integer visitparent(parentcontext ctx) { visit(ctx.expr()); setvalue(ctx,getvalue(ctx.expr())); return 0;
public Integer visitsumexpr(sumexprcontext ctx) { int l = 0; int nb = ctx.multexpr().size(); visit(ctx.multexpr(l)); Double result = getvalue(ctx.multexpr(l)); l++; int oprow = 1; while (l < nb) { visit(ctx.multexpr(l)); Double right = getvalue(ctx.multexpr(l)); result = ctx.getchild(oprow).gettext().equals("+")? result + right : result - right; l++; oprow +=2 ; setvalue(ctx, result); return 0; Utilisation d une Map Evaluation d une expression somme Liste des nœuds fils opérandes : ctx.multexpr().
Utilisation d une Map Evaluation d une expression produit public Integer visitmultexpr(multexprcontext ctx) { int l = 0; int nb = ctx.powexpr().size(); visit(ctx.powexpr(l)); Double result = getvalue(ctx.powexpr(l)); l++; int oprow = 1; while (l < nb) { visit(ctx.powexpr(l)); Double right = getvalue(ctx.powexpr(l)); result = ctx.getchild(oprow).gettext().equals("*")? result * right : result / right; l++; oprow +=2 ; setvalue(ctx, result); return 0;
Utilisation d une Map Programme public static void run_map_visitor() { try { InputStream is = new FileInputStream("prg/expr2.txt"); ANTLRInputStream stream = new ANTLRInputStream(is); Expr2Lexer lexer = new Expr2Lexer(stream); CommonTokenStream tokens = new CommonTokenStream(lexer); Expr2Parser parser = new Expr2Parser(tokens); ParseTree tree = parser.expr(); Expr2EvalMapVisitor visitor = new Expr2EvalMapVisitor(); visitor.settree(tree); visitor.visit(tree); System.out.println("Result = " + visitor.getresult()); catch(exception ex) { ex.printstacktrace();
Utilisation d une Map Résultat Expression : 5 * 3 / 2 * (15-2) Résultat : 97.5 Contrainte : chaque méthode doit stocker la valeur dans une structure ; le visitor doit mémoriser le nœud racine de l arbre. Avantage : les méthodes peuvent indiquer une information selon le type de retour choisi.
Potentiel d ANTLR Sommaire 1 Introduction Mise en œuvre d AntLR Analyses Principe de fonctionnement Grammaire v4 2 Evaluation d une expression Utilisation du paramètre de retour 3 Expressions arithmétiques Utilisation d une Map Potentiel d ANTLR
Potentiel d ANTLR Avancées de ANTLR v4 ANTLR v4 simplifie l écriture de règles de grammaire grâce à l algorithme ALL(*) Il élimine les conflits principaux des grammaires LL et donc des parser descendants : Non factorisation à gauche a : b INT b ( a ) Récursivité directe à gauche expr : expr * expr
Potentiel d ANTLR Grammaire v4 grammar Expr3; @header { package expression3; INT : 0.. 9 + ; WS : ( \r \t \n )+ {skip(); ; prg: expr ; expr: expr ^ expr #exp expr ( * / ) expr #mult expr ( + - ) expr #sum INT #int ( expr ) #parent ;
Potentiel d ANTLR Interprétation expr: expr ( * / ) expr #mult expr ( + - ) expr #sum... ; La récursivité directe ne pose pas de problème. Le premier cas est considéré prioritaire ; il faut y mettre l opérateur considéré le plus important (*, /). L associativité est à gauche. 10-6 + 4 = 8 ; 100-10 * 4 = 60
Potentiel d ANTLR Evaluation d un nombre Elle correspond à l alternative int. public Integer visitint(intcontext ctx) { String inttext = ctx.int().gettext(); Double r = Double.valueOf(intText);... return 0;
Potentiel d ANTLR Evaluation d une expression Il faut une règle qui appelle le calcul d expression lorsqu il n y a qu une alternative. public Integer visitprg(prgcontext ctx) { visit(ctx.expr());... return 0;
Potentiel d ANTLR Evaluation d une parenthèse Elle correspond à l alternative parent. public Integer visitparent(parentcontext ctx) { visit(ctx.expr());... return 0;
Potentiel d ANTLR Evaluation d une expression somme 2 nœuds fils opérandes : ctx.expr(). public Integer visitsum(sumcontext ctx) { visit(ctx.expr(0)); visit(ctx.expr(1)); Double left =... Double right =... Double result = ctx.getchild(1).gettext().equals("+")? left + right : left - right;... return 0;
Potentiel d ANTLR Evaluation d une expression produit public Integer visitmult(sumcontext ctx) { visit(ctx.expr(0)); visit(ctx.expr(1)); Double left =... Double right =... Double result = ctx.getchild(1).gettext().equals("*")? left * right : left / right;... return 0;
Potentiel d ANTLR Programme public static void run_antlr_visitor() { try { InputStream is = new FileInputStream(prog3); ANTLRInputStream stream = new ANTLRInputStream(is); ExprLexer lexer = new ExprLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lexer); ExprParser parser = new ExprParser(tokens); ParseTree tree = parser.prg(); ExprEvalRecursVisitor visitor = new ExprEvalRecursVisitor(); visitor.visit(tree); System.out.println("Result = " + visitor.getresult()); catch(exception ex) { ex.printstacktrace();