Verwenden Sie ANTLR 3.3?


72

Ich versuche, mit ANTLR und C # zu beginnen, aber ich finde es außerordentlich schwierig, da es an Dokumentation / Tutorials mangelt. Ich habe ein paar halbherzige Tutorials für ältere Versionen gefunden, aber es scheint, dass seitdem einige wichtige Änderungen an der API vorgenommen wurden.

Kann mir jemand ein einfaches Beispiel geben, wie man eine Grammatik erstellt und in einem kurzen Programm verwendet?

Ich habe es endlich geschafft, meine Grammatikdatei in einen Lexer und Parser zu kompilieren, und ich kann diese in Visual Studio kompilieren und ausführen (nachdem ich die ANTLR-Quelle neu kompilieren musste, weil die C # -Binärdateien ebenfalls veraltet zu sein scheinen! - Ganz zu schweigen davon, dass die Quelle nicht ohne einige Korrekturen kompiliert werden kann. Ich habe jedoch noch keine Ahnung, was ich mit meinen Parser / Lexer-Klassen tun soll. Angeblich kann es bei einigen Eingaben einen AST erzeugen ... und dann sollte ich in der Lage sein, etwas Besonderes daraus zu machen.

Antworten:


132

Angenommen, Sie möchten einfache Ausdrücke analysieren, die aus den folgenden Token bestehen:

  • - Subtraktion (auch unär);
  • + Zusatz;
  • * Multiplikation;
  • / Teilung;
  • (...) Gruppieren von (Unter-) Ausdrücken;
  • Ganz- und Dezimalzahlen.

Eine ANTLR-Grammatik könnte folgendermaßen aussehen:

grammar Expression;

options {
  language=CSharp2;
}

parse
  :  exp EOF 
  ;

exp
  :  addExp
  ;

addExp
  :  mulExp (('+' | '-') mulExp)*
  ;

mulExp
  :  unaryExp (('*' | '/') unaryExp)*
  ;

unaryExp
  :  '-' atom 
  |  atom
  ;

atom
  :  Number
  |  '(' exp ')' 
  ;

Number
  :  ('0'..'9')+ ('.' ('0'..'9')+)?
  ;

Um nun einen richtigen AST zu erstellen, fügen Sie ihn output=AST;in Ihren options { ... }Abschnitt ein und mischen einige "Baumoperatoren" in Ihrer Grammatik, um zu definieren, welche Token die Wurzel eines Baums sein sollen. Es gibt zwei Möglichkeiten, dies zu tun:

  1. füge hinzu ^und !nach deinen Token. Das ^bewirkt , dass das Token eine Wurzel werden und die !umfasst nicht das Token aus dem ast;
  2. durch Verwendung von "Regeln umschreiben" : ... -> ^(Root Child Child ...).

Nehmen Sie foozum Beispiel die Regel :

foo
  :  TokenA TokenB TokenC TokenD
  ;

und lassen Sie uns sagen , Sie wollen TokenBdie Wurzel werden und TokenAund TokenCseine Kinder zu werden, und Sie ausschließen möchten TokenDaus dem Baum. So geht's mit Option 1:

foo
  :  TokenA TokenB^ TokenC TokenD!
  ;

und so geht's mit Option 2:

foo
  :  TokenA TokenB TokenC TokenD -> ^(TokenB TokenA TokenC)
  ;

Hier ist die Grammatik mit den Baumoperatoren:

grammar Expression;

options {
  language=CSharp2;
  output=AST;
}

tokens {
  ROOT;
  UNARY_MIN;
}

@parser::namespace { Demo.Antlr }
@lexer::namespace { Demo.Antlr }

parse
  :  exp EOF -> ^(ROOT exp)
  ;

exp
  :  addExp
  ;

addExp
  :  mulExp (('+' | '-')^ mulExp)*
  ;

mulExp
  :  unaryExp (('*' | '/')^ unaryExp)*
  ;

unaryExp
  :  '-' atom -> ^(UNARY_MIN atom)
  |  atom
  ;

atom
  :  Number
  |  '(' exp ')' -> exp
  ;

Number
  :  ('0'..'9')+ ('.' ('0'..'9')+)?
  ;

Space 
  :  (' ' | '\t' | '\r' | '\n'){Skip();}
  ;

Ich habe auch eine SpaceRegel hinzugefügt , um Leerzeichen in der Quelldatei zu ignorieren, und einige zusätzliche Token und Namespaces für den Lexer und den Parser hinzugefügt. Beachten Sie, dass die Reihenfolge wichtig ist ( options { ... }zuerst, dann tokens { ... }und schließlich die @... {}-namespace-Deklarationen).

Das ist es.

Generieren Sie nun einen Lexer und Parser aus Ihrer Grammatikdatei:

java -cp antlr-3.2.jar org.antlr.Tool Expression.g

und fügen Sie die .csDateien in Ihrem Projekt zusammen mit den C # -Runtime-DLLs ein .

Sie können es mit der folgenden Klasse testen:

using System;
using Antlr.Runtime;
using Antlr.Runtime.Tree;
using Antlr.StringTemplate;

namespace Demo.Antlr
{
  class MainClass
  {
    public static void Preorder(ITree Tree, int Depth) 
    {
      if(Tree == null)
      {
        return;
      }

      for (int i = 0; i < Depth; i++)
      {
        Console.Write("  ");
      }

      Console.WriteLine(Tree);

      Preorder(Tree.GetChild(0), Depth + 1);
      Preorder(Tree.GetChild(1), Depth + 1);
    }

    public static void Main (string[] args)
    {
      ANTLRStringStream Input = new ANTLRStringStream("(12.5 + 56 / -7) * 0.5"); 
      ExpressionLexer Lexer = new ExpressionLexer(Input);
      CommonTokenStream Tokens = new CommonTokenStream(Lexer);
      ExpressionParser Parser = new ExpressionParser(Tokens);
      ExpressionParser.parse_return ParseReturn = Parser.parse();
      CommonTree Tree = (CommonTree)ParseReturn.Tree;
      Preorder(Tree, 0);
    }
  }
}

welches die folgende Ausgabe erzeugt:

WURZEL
  * *
    +
      12.5
      /.
        56
        UNARY_MIN
          7
    0,5

was dem folgenden AST entspricht:

Alt-Text

(Diagramm erstellt mit graph.gafol.net )

Beachten Sie, dass ANTLR 3.3 gerade veröffentlicht wurde und das CSharp-Ziel "in der Beta" ist. Deshalb habe ich in meinem Beispiel ANTLR 3.2 verwendet.

Bei eher einfachen Sprachen (wie in meinem obigen Beispiel) können Sie das Ergebnis auch im laufenden Betrieb auswerten, ohne einen AST zu erstellen. Sie können dies tun, indem Sie einfachen C # -Code in Ihre Grammatikdatei einbetten und Ihre Parserregeln einen bestimmten Wert zurückgeben lassen.

Hier ist ein Beispiel:

grammar Expression;

options {
  language=CSharp2;
}

@parser::namespace { Demo.Antlr }
@lexer::namespace { Demo.Antlr }

parse returns [double value]
  :  exp EOF {$value = $exp.value;}
  ;

exp returns [double value]
  :  addExp {$value = $addExp.value;}
  ;

addExp returns [double value]
  :  a=mulExp       {$value = $a.value;}
     ( '+' b=mulExp {$value += $b.value;}
     | '-' b=mulExp {$value -= $b.value;}
     )*
  ;

mulExp returns [double value]
  :  a=unaryExp       {$value = $a.value;}
     ( '*' b=unaryExp {$value *= $b.value;}
     | '/' b=unaryExp {$value /= $b.value;}
     )*
  ;

unaryExp returns [double value]
  :  '-' atom {$value = -1.0 * $atom.value;}
  |  atom     {$value = $atom.value;}
  ;

atom returns [double value]
  :  Number      {$value = Double.Parse($Number.Text, CultureInfo.InvariantCulture);}
  |  '(' exp ')' {$value = $exp.value;}
  ;

Number
  :  ('0'..'9')+ ('.' ('0'..'9')+)?
  ;

Space 
  :  (' ' | '\t' | '\r' | '\n'){Skip();}
  ;

was mit der Klasse getestet werden kann:

using System;
using Antlr.Runtime;
using Antlr.Runtime.Tree;
using Antlr.StringTemplate;

namespace Demo.Antlr
{
  class MainClass
  {
    public static void Main (string[] args)
    {
      string expression = "(12.5 + 56 / -7) * 0.5";
      ANTLRStringStream Input = new ANTLRStringStream(expression);  
      ExpressionLexer Lexer = new ExpressionLexer(Input);
      CommonTokenStream Tokens = new CommonTokenStream(Lexer);
      ExpressionParser Parser = new ExpressionParser(Tokens);
      Console.WriteLine(expression + " = " + Parser.parse());
    }
  }
}

und erzeugt die folgende Ausgabe:

(12,5 + 56 / -7) * 0,5 = 2,25

BEARBEITEN

In den Kommentaren schrieb Ralph:

Tipp für Benutzer von Visual Studio: Sie können so etwas wie java -cp "$(ProjectDir)antlr-3.2.jar" org.antlr.Tool "$(ProjectDir)Expression.g"in die Pre-Build-Ereignisse einfügen, dann können Sie einfach Ihre Grammatik ändern und das Projekt ausführen, ohne sich um die Neuerstellung des Lexers / Parsers kümmern zu müssen.


2
Nett! Das ist eine große Hilfe. Ich denke, ein großer Teil des Problems war, dass 3.3 einfach nicht funktioniert. Es wird parse()privat und skip()ist nicht verfügbar, und die C # -Laufzeiten funktionieren nicht damit. Dies sollte mir den Einstieg erleichtern, vielen Dank!
Mpen

Auch ... mit welchem ​​Programm haben Sie das Diagramm erstellt? Sieht gut aus :)
Mpen

3
Tipp für Benutzer von Visual Studio: Sie können so etwas wie java -cp "$(ProjectDir)antlr-3.2.jar" org.antlr.Tool "$(ProjectDir)Expression.g"in die Pre-Build-Ereignisse einfügen :) Dann können Sie einfach Ihre Grammatik ändern und das Projekt ausführen, ohne sich um die Neuerstellung des Lexers / Parsers kümmern zu müssen.
Mpen

1
Ich hatte Probleme beim Ausführen des Beispiels mit der neuesten antlr-Laufzeit (v3.4). Verwenden von language = CSharp3; scheint es zu beheben.
Dave Turvey

1
Ja, Sprache = CSharp3. Auch „Parse“ scheint nicht als Methode zu zeigen, wenn sie nicht durch „public“ in der .B - Datei, wie „startProg“ vorangestellt ist, hier: programming-pages.com/2012/07/01/...
Jared

13

Haben Sie sich Irony.net angesehen ? Es richtet sich an .Net und funktioniert daher sehr gut, verfügt über geeignete Werkzeuge, geeignete Beispiele und funktioniert einfach. Das einzige Problem ist, dass es immer noch ein bisschen "alpha-ish" ist, so dass sich Dokumentation und Versionen ein wenig zu ändern scheinen, aber wenn Sie sich nur an eine Version halten, können Sie raffinierte Dinge tun.

ps Entschuldigung für die schlechte Antwort, bei der Sie ein Problem mit X stellen und jemand mit Y etwas anderes vorschlägt; ^)


Hrm ... im Allgemeinen wäre ich beleidigt, dass Sie die Frage ignoriert haben, aber diese Ironie scheint tatsächlich ziemlich nett während meines vorläufigen Stocherns zu sein :)
mpen

@Ralph, bist du wirklich "beleidigt", wenn jemand eine Antwort veröffentlicht, die deine Frage nicht zu 100% beantwortet?
Bart Kiers

@Bart: Ich bin mir nicht sicher, ob "beleidigt" genau das Wort ist, nach dem ich suche ... aber [normalerweise] finde ich es etwas ärgerlich, wenn jemand über etwas völlig anderes schreibt, besonders wenn ich es bereits in Betracht gezogen habe. und entschied sich dagegen.
Mpen

@ Ralph, ja, ich kann mir dein Gefühl vorstellen, aber persönlich hätte ich das Wort "beleidigt" nicht verwendet, das ist alles. Es ist nicht so, dass Toads Antwort völlig vom Thema abwich.
Bart Kiers

2
Seit 2014 ist Irony dem bezahlten Job des Entwicklers in den Hintergrund getreten.
Alan B

8

Meine persönliche Erfahrung ist, dass Sie vor dem Erlernen von ANTLR auf C # /. NET genügend Zeit haben sollten, um ANTLR auf Java zu lernen. Das gibt Ihnen Wissen über alle Bausteine ​​und später können Sie sich auf C # /. NET bewerben.

Ich habe kürzlich ein paar Blog-Beiträge geschrieben,

Es wird davon ausgegangen, dass Sie mit ANTLR unter Java vertraut sind und bereit sind, Ihre Grammatikdatei nach C # /. NET zu migrieren.


1
Was ist der Vorteil, wenn man zuerst auf Java lernt? Wie wäre es einfacher (abgesehen von mehr Tutorials)? Besonders wenn mein Java nicht so stark ist wie mein C #?
Mpen

3
Sie müssen kein Java-Experte sein, um die Java-Tutorials zu beenden. Für mich brachte es mir die ABCs von ANTLR bei, und dann konnte ich schnell wissen, wie ANTLR funktioniert, und wieder zu C # wechseln.
Lex Li

4

Hier gibt es einen großartigen Artikel darüber, wie man Antlr und C # zusammen verwendet:

http://www.codeproject.com/KB/recipes/sota_expression_evaluator.aspx

Es handelt sich um einen Artikel des Erstellers von NCalc, einem Bewerter für mathematische Ausdrücke für C # - http://ncalc.codeplex.com

Sie können die Grammatik für NCalc auch hier herunterladen: http://ncalc.codeplex.com/SourceControl/changeset/view/914d819f2865#Grammar%2fNCalc.g

Beispiel für die Funktionsweise von NCalc:

Expression e = new Expression("Round(Pow(Pi, 2) + Pow([Pi2], 2) + X, 2)"); 

  e.Parameters["Pi2"] = new Expression("Pi * Pi"); 
  e.Parameters["X"] = 10; 

  e.EvaluateParameter += delegate(string name, ParameterArgs args) 
    { 
      if (name == "Pi") 
      args.Result = 3.14; 
    }; 

  Debug.Assert(117.07 == e.Evaluate()); 

hoffe es ist hilfreich

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.