37

Ich habe viel darüber gesucht und ich konnte nichts nützliches finden, das mir wirklich hilft, einen AST zu bauen. Ich weiß bereits, dass ANTLR4 nicht AST wie ANTLR3 erstellt. Jeder sagt: "Hey, Besucher benutzen!", Aber ich konnte kein Beispiel oder eine ausführlichere Erklärung finden, wie ich das tun kann ...Wie erstelle ich AST mit ANTLR4?

Ich habe eine Grammatik muss wie C, aber mit jedem Befehl geschrieben in Portugiesisch (Portugiesisch Programmiersprache). Ich kann den Syntaxbaum leicht mit ANTLR4 erzeugen. Meine Frage ist: Was muss ich jetzt tun, um eine AST zu erstellen?

BTW, ich bin mit Java und IntelliJ ...

EDIT1: Die nächstgelegene ich bekommen konnte die Antwort zu diesem Thema wurde mit: Is there a simple example of using antlr4 to create an AST from java source code and extract methods, variables and comments? Aber es druckt nur den Namen der besuchten Methoden. der erste Versuch für mich nicht funktioniert, wie ich erwartet habe, habe ich versucht, this tutorial von antlr3 zu verwenden, aber ich kann nicht herausfinden, wie StringTamplate statt ST verwenden ...

Seit

des Lesen. Buch The Definitive ANTLR 4 Reference I konnte auch nichts im Zusammenhang mit ASTs finden.

EDIT2: Jetzt habe ich eine Klasse haben die DOT-Datei zu erstellen, muss ich nur noch herausfinden, wie die Besucher richtig nutzen

+1

Könnten Sie einige was Sie versucht haben? –

+0

@SandyGifford Ich habe meinen Beitrag bearbeitet und versucht zu erklären ... Ich habe meinen Code gerade nicht, weil ich gerade gelöscht habe, was ich gemacht habe. Im Moment habe ich nur die generierten Codes von ATNLR4 (Parser, Lexer und Base Besucher und Listener) –

+0

Leider weiß ich nichts über ANTLR (Sie kamen in meine Warteschlange), aber Sie haben die Qualität des Beitrags erhöht! –

Antwort

90

Ok, lassen Sie uns ein einfaches Mathe Beispiel bauen. Der Aufbau eines AST ist völlig übertrieben für eine solche Aufgabe, 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 zu arbeiten:

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

Ziemlich einfach Sachen, wir eine einzige expr Regel, die alles (Vorrangregeln usw.) behandelt.

Die AST-Knoten

Dann lassen Sie uns einige AST-Knoten definieren wir verwenden. Diese sind völlig benutzerdefiniert und Sie können sie so definieren, wie Sie es möchten.

Hier sind die Knoten wir für dieses Beispiel verwendet 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 mit einem AST

ANTLR die CST-Knoten für uns erzeugt (die MathParser.*Context Klassen). Wir müssen diese nun in AST-Knoten umwandeln.

Dies ist leicht mit einem Besucher getan, und ANTLR bietet uns eine MathBaseVisitor<T> Klasse, also lasst uns 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, es ist nur eine Frage von einem AST-Knoten aus einem CST-Knoten zu schaffen, indem einen Besucher verwendet.Der Code sollte ziemlich selbsterklärend sein (gut, vielleicht außer für die VisitFuncExpr Sachen, aber es ist nur eine schnelle Möglichkeit, einen Delegaten zu einer geeigneten Methode der System.Math Klasse zu verdrahten).

Und hier haben Sie die AST Gebäude Zeug. Das ist alles was benötigt wird. Extrahieren Sie einfach die relevanten Informationen aus dem CST und behalten Sie sie im AST.

Der AST Besucher

Nun lassen Sie uns mit dem AST ein bisschen spielen. Wir müssen eine AST-Besucherbasisklasse erstellen, um sie zu durchlaufen. Lassen Sie uns etwas tun, das dem von ANTLR bereitgestellten AbstractParseTreeVisitor<T> ähnlich ist.

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 nutzte ich C# 's dynamic Schlüsselwort eine Doppel Versand in einer Zeile Code auszuführen. In Java, werden Sie die Verkabelung selbst mit einer Folge von if Aussagen wie diese zu tun haben:

if (node is AdditionNode) { 
    return Visit((AdditionNode)node); 
} else if (node is SubtractionNode) { 
    return Visit((SubtractionNode)node); 
} else if ... 

Aber ich ging nur für die Verknüpfung für dieses Beispiel.

Die Arbeit mit dem AST

Also, was können wir mit einem mathematischen Ausdrucksbaum zu tun? Bewerten Sie es natürlich! Lassen Sie sich einen Ausdrucksauswerter implementieren:

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, sobald wir einen AST haben, ist es nicht?

setzen sie alle zusammen

Last but not least, müssen wir eigentlich das Hauptprogramm 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 mit ihm spielen:

enter image description here

+2

Vielen Dank! Es wird mir sehr helfen ... –

+0

Diese Methode ist ziemlich manuelle Arbeit. Gibt es eine andere Lösung, die keinen Code für jede Art von Knoten erstellen muss? – Waschbaer

+1

@Waschbaer IMO die Feinabstimmung der Knoten manuell verbessert die Wartbarkeit und ist es wert, aber Sie können anderer Meinung sein. ANTLR 3 hatte einen AST-Ausgabemodus, also könnten Sie diesen verwenden, wenn Sie mit der Verwendung eines veralteten Tools vertraut sind. Oder vielleicht könnten Sie Metaprogrammierung/Templating verwenden, um einen Standardcode zu generieren, aber am Ende werden Sie wahrscheinlich mehr Arbeit haben. Eine andere Möglichkeit besteht darin, direkt mit dem CST zu arbeiten. –

2

Ich habe ein kleines Java-Projekt erstellt, mit dem Sie Ihre ANTLR-Grammatik sofort testen können, indem Sie den von ANT generierten Lexer und Parser kompilieren LR im Speicher. Sie können einen String einfach parsen, indem Sie ihn an den Parser übergeben, und daraus wird automatisch ein AST generiert, der dann in Ihrer Anwendung verwendet werden kann.

Um die Größe der AST zu reduzieren, können Sie einen NodeFilter verwenden, zu dem Sie die Produktionsregelnamen der Nicht-Terminals hinzufügen können, die beim Erstellen der AST berücksichtigt werden sollen.

Der Code und einige Code-Beispiele finden Sie unter https://github.com/julianthome/inmemantlr

das Werkzeug nützlich ist ;-) Hoffnung finden

+0

Ich habe versucht, mit Ihrem "kleinen" Projekt, aber Sie haben Hunderte von Dateien ohne Kommentare, es ist unmöglich herauszufinden, was los ist. Sie haben alles in Ihrer eigenen Version von Wrapper-Funktionen verpackt. Ein Benutzer müsste Ihr gesamtes Projekt herunterladen und unverändert verwenden und lernen, Ihre neuen Klassen (GenericParser ??) zu verwenden. Ich kann Ihren Code nicht verwenden, um herauszufinden, wie ich meinen eigenen AST in meinem eigenen Code erstellen kann. –

+0

Hi John inmemantlr's Code-Basis besteht aus 48 JavaClasses ('finde inmemantlr-api/src/main -name" * .java "| nl'), von denen die meisten ziemlich gut kommentiert sind (http://www.javadoc.io/ doc/com.github.julianthome/inmemantlr-api/1.3.9). Um die oben erwähnten Punkte (API-Verwendung, ParseTree-Erstellung) zu veranschaulichen, habe ich Erklärungen in der Datei 'README.md' und Testfälle in https://github.com/julianthome/inmemantlr/tree/master/inmemantlr bereitgestellt -api/src/test/java. Sollten Sie jedoch Probleme mit dem Tool haben, helfen wir Ihnen gerne weiter.Bitte senden Sie mir eine E-Mail oder erstellen Sie ein Problem auf GitHub. – Julian

+0

Könnte es sein, dass Sie das grammars-v4-Submodul gezogen haben (siehe "inmemantlr-api/src/test/resources/grammars-v4")? Eigentlich ist dieses Modul nicht Bestandteil der Code-Basis von inemantlr; Es wird verwendet, um sicherzustellen, dass inmemantlr an allen grammars-v4-Grammatiken arbeitet. Das Submodul wird jedoch nicht standardmäßig bei der Ausführung von 'git clone https: // github.com/julianthome/inemantlr' abgerufen. – Julian