Ich habe VIEL darüber gesucht und konnte nichts Nützliches finden, das mir WIRKLICH hilft, einen AST aufzubauen. Ich weiß bereits, dass ANTLR4 AST nicht mehr so erstellt wie früher ANTLR3. Alle sagen: "Hey, benutze Besucher!", Aber ich konnte kein Beispiel oder eine detailliertere Erklärung finden, wie ich das machen kann ...
Ich habe eine Grammatik muss C mögen, aber mit allen Befehlen in Portugiesisch (portuga Programmiersprache) geschrieben. Ich kann den Analysebaum einfach mit ANTLR4 generieren. Meine Frage ist: Was muss ich jetzt tun, um einen AST zu erstellen?
Übrigens benutze ich Java und IntelliJ ...
EDIT1: Am ehesten konnte ich die Antwort auf dieses Thema verwenden: Gibt es ein einfaches Beispiel für die Verwendung von antlr4, um einen AST aus Java-Quellcode zu erstellen und Methoden, Variablen und Kommentare zu extrahieren? Es wird jedoch nur der Name der besuchten Methoden gedruckt.
Da der erste Versuch bei mir nicht wie erwartet funktioniert hat, habe ich versucht, dieses Tutorial von ANTLR3 zu verwenden, aber ich konnte nicht herausfinden, wie StringTamplate anstelle von ST verwendet wird ...
Lesen des Buches The Definitive ANTLR 4 Reference Ich konnte auch nichts im Zusammenhang mit ASTs finden.
EDIT2: Jetzt habe ich eine Klasse zum Erstellen der DOT-Datei. Ich muss nur noch herausfinden, wie Besucher richtig verwendet werden
Antworten:
Ok, lassen Sie uns ein einfaches mathematisches Beispiel erstellen. Das Erstellen eines AST ist für eine solche Aufgabe völlig übertrieben, aber es ist eine gute Möglichkeit, das Prinzip zu zeigen.
Ich werde es in C # tun, aber die Java-Version wäre sehr ähnlich.
Die Grammatik
Lassen Sie uns zunächst eine sehr grundlegende mathematische Grammatik schreiben, mit der Sie arbeiten können:
Ziemlich einfaches Zeug, wir haben eine Single
expr
Regel, die alles behandelt (Vorrangregeln usw.).Die AST-Knoten
Definieren wir dann einige AST-Knoten, die wir verwenden werden. Diese sind vollständig benutzerdefiniert und können nach Ihren Wünschen definiert werden.
Hier sind die Knoten, die wir für dieses Beispiel verwenden werden:
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; } }
Konvertieren eines CST in einen AST
ANTLR hat die CST-Knoten für uns generiert (die
MathParser.*Context
Klassen) . Wir müssen diese nun in AST-Knoten konvertieren.Dies ist mit einem Besucher leicht möglich, und ANTLR bietet uns eine
MathBaseVisitor<T>
Klasse an. Lassen Sie uns also damit arbeiten.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()) }; } }
Wie Sie sehen können, müssen Sie lediglich einen AST-Knoten aus einem CST-Knoten mithilfe eines Besuchers erstellen. Der Code sollte ziemlich selbsterklärend sein (naja, vielleicht mit Ausnahme des
VisitFuncExpr
Materials, aber es ist nur eine schnelle Möglichkeit, einen Delegierten mit einer geeigneten Methode derSystem.Math
Klasse zu verbinden).Und hier haben Sie das AST-Baumaterial. Das ist alles was benötigt wird. Extrahieren Sie einfach die relevanten Informationen aus dem CST und bewahren Sie sie im AST auf.
Der AST-Besucher
Lassen Sie uns jetzt ein bisschen mit dem AST spielen. Wir müssen eine AST-Besucherbasisklasse erstellen, um sie zu durchlaufen. Lassen Sie uns einfach etwas Ähnliches wie das
AbstractParseTreeVisitor<T>
von ANTLR bereitgestellte tun .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); } }
Hier habe ich das
dynamic
Schlüsselwort von C # genutzt, um einen Doppelversand in einer Codezeile durchzuführen. In Java müssen Sie die Verkabelung selbst mit einer Folge vonif
Anweisungen wie diesen durchführen:if (node is AdditionNode) { return Visit((AdditionNode)node); } else if (node is SubtractionNode) { return Visit((SubtractionNode)node); } else if ...
Aber ich habe mich gerade für die Verknüpfung für dieses Beispiel entschieden.
Arbeiten Sie mit dem AST
Was können wir also mit einem mathematischen Ausdrucksbaum tun? Bewerten Sie es natürlich! Implementieren wir einen Ausdrucksauswerter:
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; } }
Ziemlich einfach, wenn wir einen AST haben, nicht wahr?
Alles zusammenfügen
Zu guter Letzt müssen wir das Hauptprogramm tatsächlich schreiben:
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(); } } }
Und jetzt können wir endlich damit spielen:
quelle
QueryBaseVisitor
(es sei denn, Ihr Grammatikname enthält diesenParser
Teil, aber in diesem Fall haben Sie auch eineQueryParserParser
). IIRC Das NuGet-Paket generiert es automatisch. Wenn Sie ANTLR jedoch manuell ausführen, müssen Sie die-visitor
Option hinzufügen .Visit
Methoden haben nichts miteinander zu tun, sie haben zufällig den gleichen Namen, aber da beide Klassen Besucher sind, verwenden sie den gleichen Namen :)Ich habe ein kleines Java-Projekt erstellt, mit dem Sie Ihre ANTLR-Grammatik sofort testen können, indem Sie den von ANTLR im Speicher generierten Lexer und Parser kompilieren. Sie können eine Zeichenfolge einfach analysieren, indem Sie sie an den Parser übergeben. Daraus wird automatisch ein AST generiert, der dann in Ihrer Anwendung verwendet werden kann.
Um die Größe des AST zu verringern, können Sie einen NodeFilter verwenden, zu dem Sie die Produktionsregelnamen der Nicht-Terminals hinzufügen können, die Sie beim Erstellen des AST berücksichtigen möchten.
Der Code und einige Codebeispiele finden Sie unter https://github.com/julianthome/inmemantlr
Hoffe das Tool ist nützlich ;-)
quelle
find inmemantlr-api/src/main -name "*.java" | nl
), von denen die meisten ziemlich gut kommentiert sind ( javadoc.io/doc/com.github.julianthome/inmemantlr-api/1.3.9 ). Um die oben genannten Punkte zu veranschaulichen (API-Verwendung, ParseTree-Erstellung), habe ich Erklärungen in denREADME.md
Testfällen bereitgestellt und Testfälle unter github.com/julianthome/inmemantlr/tree/master/inmemantlr-api/… bereitgestellt . Falls Sie jedoch Probleme mit dem Tool haben, helfen wir Ihnen gerne weiter. Bitte schreiben Sie mir einfach eine E-Mail oder erstellen Sie ein Problem auf Github.inmemantlr-api/src/test/resources/grammars-v4
)? Tatsächlich ist dieses Modul nicht Teil der Codebasis von inmemantlr. Es wird verwendet, um sicherzustellen, dass inmemantlr auf allen Grammatiken-v4-Grammatiken funktioniert. Das Submodul wird jedoch bei der Ausführung nicht standardmäßig abgerufengit clone https://github.com/julianthome/inmemantlr
.Ich habe zwei einfache Möglichkeiten gefunden, das sich auf die Funktionalität in der TestRig.java Datei von antlr4.
Dies ist mein Beispiel für das Parsen von C ++ mit der entsprechenden CPP14.g4-Grammatikdatei
java -cp .:antlr-4.9-complete.jar org.antlr.v4.gui.TestRig CPP14 translationunit -tree filename.cpp
. Wenn Sie die Datei filename.cpp weglassen, liest das Rig von stdin. "translationunit" ist der Name der Startregel der von mir verwendeten Grammatikdatei CPP14.g4 .Ich habe Teile des Codes verwendet, die in der Datei TestRig.java enthalten sind. Nehmen wir noch einmal an, wir haben eine Zeichenfolge aus C ++ - Quellcode, aus der wir den AST erzeugen möchten (Sie können auch direkt aus einer Datei lesen).
String source_code = "...your cpp source code..."; CodePointCharStream stream_from_string = CharStreams.fromString(source_code); CPP14Lexer lexer = new CPP14Lexer(new ANTLRInputStream(source_code)); CommonTokenStream tokens = new CommonTokenStream(lexer); CPP14Parser parser = new CPP14Parser(tokens); String parserName = "CPP14Parser"; ClassLoader cl = Thread.currentThread().getContextClassLoader(); Class<? extends Parser> parserClass = null; parserClass = cl.loadClass(parserName).asSubclass(Parser.class); String startRuleName = "translationunit"; //as specified in my CPP14.g4 file Method startRule = parserClass.getMethod(startRuleName); ParserRuleContext tree = (ParserRuleContext)startRule.invoke(parser, (Object[])null); System.out.println(tree.toStringTree(parser));
Meine Importe sind:
import java.lang.reflect.Method; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CodePointCharStream; import org.antlr.v4.runtime.ANTLRInputStream; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.Parser;
All dies setzt voraus, dass Sie die erforderlichen Dateien (Lexer, Parser usw.) mit dem Befehl erstellt
java -jar yournaltrfile.jar yourgrammar.g4
und anschließend alle * .java-Dateien kompiliert haben.quelle