Использование ANTLR 3.3?

Я пытаюсь начать работу с ANTLR и С#, но я нахожу это чрезвычайно сложным из-за отсутствия документации/учебников. Я нашел пару половинчатых учебников для более старых версий, но, похоже, с API были некоторые существенные изменения.

Может ли кто-нибудь дать мне простой пример того, как создать грамматику и использовать ее в короткой программе?

Наконец-то мне удалось собрать компиляцию файла грамматики в лексер и синтаксический анализатор, и я могу получить их скомпилированные и запущенные в Visual Studio (после перекомпиляции источника ANTLR, поскольку двоичные файлы С#, похоже, устарели! - не говоря уже о том, что источник не компилируется без каких-либо исправлений), но я до сих пор не знаю, что делать с моими классами парсера /lexer. Предположительно, это может привести к тому, что AST даст некоторый вклад... и тогда я должен был бы с этим поработать.

Ответ 1

Предположим, вы хотите разобрать простые выражения, состоящие из следующих токенов:

  • - вычитание (также унарное);
  • + дополнение;
  • * умножение;
  • / деление;
  • (...) группировки (под) выражений;
  • целочисленные и десятичные числа.

Грамматика ANTLR может выглядеть так:

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')+)?
  ;

Теперь, чтобы создать правильный АСТ, вы добавляете output=AST; в свой раздел options { ... }, и вы смешиваете некоторые "древовидные операторы" в своей грамматике, определяя, какие токены должны быть корнем дерева. Существует два способа сделать это:

  • добавьте ^ и ! после ваших токенов. ^ приводит к тому, что токен становится корнем, а ! исключает токен от ast;
  • с помощью "правил перезаписи": ... -> ^(Root Child Child ...).

Возьмите правило foo, например:

foo
  :  TokenA TokenB TokenC TokenD
  ;

и скажем, что вы хотите, чтобы TokenB стал корнем и TokenA и TokenC, чтобы стать его дочерними элементами, и вы хотите исключить TokenD из дерева. Здесь, как это сделать, используя опцию 1:

foo
  :  TokenA TokenB^ TokenC TokenD!
  ;

и здесь, как это сделать, используя опцию 2:

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

Итак, здесь грамматика с операторами дерева в ней:

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

Я также добавил правило Space, чтобы игнорировать любые пробелы в исходном файле и добавил некоторые дополнительные токены и пространства имен для lexer и parser. Обратите внимание, что порядок важен (сначала options { ... }, затем tokens { ... } и, наконец, объявления @... {} -namespace).

Что это.

Теперь создайте лексер и парсер из файла грамматики:

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

и поместите файлы .cs в проект вместе с С# runtime DLL.

Вы можете протестировать его, используя следующий класс:

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

который производит следующий вывод:

ROOT
  *
    +
      12.5
      /
        56
        UNARY_MIN
          7
    0.5

что соответствует следующему AST:

alt text

(диаграмма, созданная с помощью graph.gafol.net)

Обратите внимание, что ANTLR 3.3 только что был выпущен, а цель CSharp - "в бета-версии". Вот почему я использовал ANTLR 3.2 в моем примере.

В случае довольно простых языков (например, мой пример выше) вы также можете оценивать результат "на лету" без создания АСТ. Вы можете сделать это, вставив обычный код С# в свой файл грамматики и позволяя правилам парсера возвращать определенное значение.

Вот пример:

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

который можно протестировать с помощью класса:

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

и выводит следующий результат:

(12.5 + 56 / -7) * 0.5 = 2.25

ИЗМЕНИТЬ

В комментариях Ральф писал:

Совет для тех, кто использует Visual Studio: вы можете поместить что-то вроде java -cp "$(ProjectDir)antlr-3.2.jar" org.antlr.Tool "$(ProjectDir)Expression.g" в события предварительной сборки, тогда вы можете просто изменить свою грамматику и запустить проект, не беспокоясь о восстановлении lexer/parser.

Ответ 2

Вы посмотрели Irony.net? Он нацелен на .Net и поэтому работает очень хорошо, имеет правильную оснастку, правильные примеры и просто работает. Единственная проблема заключается в том, что она по-прежнему немного "альфа-иш", поэтому документация и версии, похоже, немного меняются, но если вы просто придерживаетесь версии, вы можете делать отличные вещи.

p.s. извините за плохой ответ, где вы задаете проблему X, и кто-то предлагает что-то другое, используя Y; ^)

Ответ 3

Мой личный опыт заключается в том, что перед изучением ANTLR на С#/.NET вы должны сэкономить достаточно времени, чтобы узнать ANTLR на Java. Это дает вам знания обо всех строительных блоках, а позже вы можете применять их на С#/. NET.

Недавно я написал несколько сообщений в блоге,

Предполагается, что вы знакомы с ANTLR на Java и готовы перенести свой файл грамматики на С#/. NET.

Ответ 4

Существует отличная статья о том, как использовать здесь antlr и С#:

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

это статья "как это было сделано" создателем NCalc, который является оценщиком математических выражений для С# - http://ncalc.codeplex.com

Вы также можете скачать грамматику для NCalc здесь: http://ncalc.codeplex.com/SourceControl/changeset/view/914d819f2865#Grammar%2fNCalc.g

пример работы 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()); 

надеюсь, что это будет полезно