Comment créer AST avec ANTLR4?
j'ai beaucoup cherché à ce sujet et je n'ai rien trouvé d'utile qui m'aide vraiment à construire un AST. Je sais déjà QU'ANTLR4 ne construit pas AST comme ANTLR3 le faisait. Tout le monde dit: "Hé, utilisez des visiteurs!", mais je n'ai trouvé aucun exemple ou explication plus détaillée sur la façon de faire cela...
j'ai une grammaire doit aimer C, mais avec chaque commande écrite en portugais (portugais langue de programmation). Je peux facilement générer L'arbre parse en utilisant ANTLR4. Ma question Est: Que dois-je faire maintenant pour créer un AST?
BTW, J'utilise Java et IntelliJ...
edit 1: le plus proche que j'ai pu obtenir était d'utiliser la réponse de ce sujet: y a-t-il un exemple simple d'utilisation d'antlr4 pour créer un AST à partir de code source java et extraire des méthodes, des variables et des commentaires? Mais il n'imprime que le nom des méthodes visitées..
Depuis la première tentative n'a pas fonctionné pour moi comme je m'y attendais, j'ai essayé d'utiliser ce tutoriel D'ANTLR3, mais je ne pouvais pas comprendre comment utiliser StringTamplate au lieu de ST...
la Lecture du livre Définitive ANTLR 4 Référence je ne pouvais pas trouver tout ce qui est lié à l'ASTs.
Edit 2: maintenant j'ai une classe pour créer le fichier de points, j'ai juste besoin de comprendre sur la façon d'utiliser les visiteurs correctement
2 réponses
OK, construisons un exemple mathématique simple. Construire un AST est totalement exagéré pour une telle tâche, mais c'est une belle façon de montrer le principe.
je vais le faire en C# mais la version Java serait très similaire.
la grammaire
d'abord, écrivons une grammaire mathématique très basique pour travailler avec:
grammar Math;
compileUnit
: expr EOF
;
expr
: '(' expr ')' # parensExpr
| op=('+'|'-') expr # unaryExpr
| left=expr op=('*'|'/') right=expr # infixExpr
| left=expr op=('+'|'-') right=expr # infixExpr
| func=ID '(' expr ')' # funcExpr
| value=NUM # numberExpr
;
OP_ADD: '+';
OP_SUB: '-';
OP_MUL: '*';
OP_DIV: '/';
NUM : [0-9]+ ('.' [0-9]+)? ([eE] [+-]? [0-9]+)?;
ID : [a-zA-Z]+;
WS : [ \t\r\n] -> channel(HIDDEN);
assez basique, nous avons une seule règle expr
qui gère tout (les règles de priorité, etc).
les noeuds AST
alors, définissons quelques noeuds AST que nous utiliserons. Ces sont totalement personnalisé, et vous pouvez définir de la manière que vous voulez.
Voici les noeuds que nous allons utiliser pour cet exemple:
internal abstract class ExpressionNode
{
}
internal abstract class InfixExpressionNode : ExpressionNode
{
public ExpressionNode Left { get; set; }
public ExpressionNode Right { get; set; }
}
internal class AdditionNode : InfixExpressionNode
{
}
internal class SubtractionNode : InfixExpressionNode
{
}
internal class MultiplicationNode : InfixExpressionNode
{
}
internal class DivisionNode : InfixExpressionNode
{
}
internal class NegateNode : ExpressionNode
{
public ExpressionNode InnerNode { get; set; }
}
internal class FunctionNode : ExpressionNode
{
public Func<double, double> Function { get; set; }
public ExpressionNode Argument { get; set; }
}
internal class NumberNode : ExpressionNode
{
public double Value { get; set; }
}
conversion d'un CST en un AST
ANTLR a généré les noeuds CST pour nous (les classes MathParser.*Context
). Nous devons maintenant les convertir en AST. nœud.
cela se fait facilement avec un visiteur, et ANTLR nous fournit une classe MathBaseVisitor<T>
, donc travaillons avec cela.
internal class BuildAstVisitor : MathBaseVisitor<ExpressionNode>
{
public override ExpressionNode VisitCompileUnit(MathParser.CompileUnitContext context)
{
return Visit(context.expr());
}
public override ExpressionNode VisitNumberExpr(MathParser.NumberExprContext context)
{
return new NumberNode
{
Value = double.Parse(context.value.Text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent)
};
}
public override ExpressionNode VisitParensExpr(MathParser.ParensExprContext context)
{
return Visit(context.expr());
}
public override ExpressionNode VisitInfixExpr(MathParser.InfixExprContext context)
{
InfixExpressionNode node;
switch (context.op.Type)
{
case MathLexer.OP_ADD:
node = new AdditionNode();
break;
case MathLexer.OP_SUB:
node = new SubtractionNode();
break;
case MathLexer.OP_MUL:
node = new MultiplicationNode();
break;
case MathLexer.OP_DIV:
node = new DivisionNode();
break;
default:
throw new NotSupportedException();
}
node.Left = Visit(context.left);
node.Right = Visit(context.right);
return node;
}
public override ExpressionNode VisitUnaryExpr(MathParser.UnaryExprContext context)
{
switch (context.op.Type)
{
case MathLexer.OP_ADD:
return Visit(context.expr());
case MathLexer.OP_SUB:
return new NegateNode
{
InnerNode = Visit(context.expr())
};
default:
throw new NotSupportedException();
}
}
public override ExpressionNode VisitFuncExpr(MathParser.FuncExprContext context)
{
var functionName = context.func.Text;
var func = typeof(Math)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.ReturnType == typeof(double))
.Where(m => m.GetParameters().Select(p => p.ParameterType).SequenceEqual(new[] { typeof(double) }))
.FirstOrDefault(m => m.Name.Equals(functionName, StringComparison.OrdinalIgnoreCase));
if (func == null)
throw new NotSupportedException(string.Format("Function {0} is not supported", functionName));
return new FunctionNode
{
Function = (Func<double, double>)func.CreateDelegate(typeof(Func<double, double>)),
Argument = Visit(context.expr())
};
}
}
comme vous pouvez le voir, il s'agit simplement de créer un noeud AST à partir d'un noeud CST en utilisant un visiteur. Le code devrait être assez explicite (Eh bien, peut-être à l'exception de la VisitFuncExpr
stuff, mais c'est juste un moyen rapide de relier un délégué à une méthode appropriée de la System.Math
classe.)
et ici vous avez les matériaux de construction AST. C'est tout ce qui est nécessaire. Il suffit d'extraire les informations pertinentes du CST et de les conserver dans le TSA.
L'AST visiteur
maintenant, jouons un peu avec L'AST. Nous devrons construire une classe de base pour visiteurs AST pour la traverser. Faisons quelque chose de similaire au AbstractParseTreeVisitor<T>
fourni par ANTLR.
internal abstract class AstVisitor<T>
{
public abstract T Visit(AdditionNode node);
public abstract T Visit(SubtractionNode node);
public abstract T Visit(MultiplicationNode node);
public abstract T Visit(DivisionNode node);
public abstract T Visit(NegateNode node);
public abstract T Visit(FunctionNode node);
public abstract T Visit(NumberNode node);
public T Visit(ExpressionNode node)
{
return Visit((dynamic)node);
}
}
ici, j'ai profité de C # 'S dynamic
mot-clé pour effectuer une double-expédition dans une ligne de code. En Java, vous aurez à faire le câblage vous-même avec une séquence de if
déclarations comme celles-ci:
if (node is AdditionNode) {
return Visit((AdditionNode)node);
} else if (node is SubtractionNode) {
return Visit((SubtractionNode)node);
} else if ...
Mais je suis juste allé pour le raccourci pour cet exemple.
travailler avec les AST
alors, que pouvons-nous faire avec un arbre d'expression mathématique? L'évaluer, bien sûr! Mettons en place un évaluateur d'expressions:
internal class EvaluateExpressionVisitor : AstVisitor<double>
{
public override double Visit(AdditionNode node)
{
return Visit(node.Left) + Visit(node.Right);
}
public override double Visit(SubtractionNode node)
{
return Visit(node.Left) - Visit(node.Right);
}
public override double Visit(MultiplicationNode node)
{
return Visit(node.Left) * Visit(node.Right);
}
public override double Visit(DivisionNode node)
{
return Visit(node.Left) / Visit(node.Right);
}
public override double Visit(NegateNode node)
{
return -Visit(node.InnerNode);
}
public override double Visit(FunctionNode node)
{
return node.Function(Visit(node.Argument));
}
public override double Visit(NumberNode node)
{
return node.Value;
}
}
assez simple une fois que nous avons un AST, n'est-ce pas?
Mise au point
le Dernier mais non le moindre, nous devons écrire le programme principal:
internal class Program
{
private static void Main()
{
while (true)
{
Console.Write("> ");
var exprText = Console.ReadLine();
if (string.IsNullOrWhiteSpace(exprText))
break;
var inputStream = new AntlrInputStream(new StringReader(exprText));
var lexer = new MathLexer(inputStream);
var tokenStream = new CommonTokenStream(lexer);
var parser = new MathParser(tokenStream);
try
{
var cst = parser.compileUnit();
var ast = new BuildAstVisitor().VisitCompileUnit(cst);
var value = new EvaluateExpressionVisitor().Visit(ast);
Console.WriteLine("= {0}", value);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.WriteLine();
}
}
}
et maintenant nous pouvons enfin jouer avec:
j'ai créé un petit projet Java qui vous permet de tester votre grammaire ANTLR instantanément en compilant le lexer et l'analyseur généré par ANTLR in-memory. Vous pouvez simplement analyser une chaîne de caractères en la passant à l'analyseur, et il générera automatiquement un AST à partir de celui-ci qui pourra ensuite être utilisé dans votre application.
dans le but de réduire la taille de L'AST, vous pourriez utiliser un NodeFilter à laquelle vous pourriez ajouter les noms de règle de production des non-terminaux qui vous souhaitez être pris en compte lors de la construction de L'AST.
le code et quelques exemples de code peuvent être trouvés à https://github.com/julianthome/inmemantlr
Espère que l'outil est utile ;-)