Допустим, вы хотите разобрать простые выражения, состоящие из следующих токенов:
-
вычитание (также унарное); +
сложение; *
умножение; /
деление; (...)
группирование (под) выражений; - целые и десятичные числа.
Грамматика 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')+)?
;
Теперь, чтобы создать правильный AST, вы добавляете output=AST;
в свой раздел options { ... }
и смешиваете некоторые "операторы дерева" в своей грамматике.определить, какие токены должны быть корнем дерева.Есть два способа сделать это:
- добавить
^
и !
после ваших токенов.^
приводит к тому, что токен становится корнем, а !
исключает токен из ast; - с помощью "правил перезаписи":
... -> ^(Root Child Child ...)
.
Takeправило 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
, чтобы игнорировать любые пробелы в исходном файле, и добавил некоторые дополнительные токены и пространства имен для лексера и анализатора.Обратите внимание, что порядок важен (сначала options { ... }
, затем tokens { ... }
и, наконец, объявления @... {}
-namespace).
Вот и все.
Теперь сгенерируйте лексер и парсер из вашегофайл грамматики:
java -cp antlr-3.2.jar org.antlr.Tool Expression.g
и поместите файлы .cs
в свой проект вместе с DLL-библиотеками времени выполнения *1069*.
. Вы можете проверить это с помощью следующего класса:
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:
(диаграмма, созданная с использованием graph.gafol.net )
Обратите внимание, что ANTLR 3.3 только что выпущен, а цель CSharp находится "в бета-версии".Вот почему я использовал ANTLR 3.2 в моем примере.
В случае довольно простых языков (как мой пример выше), вы также можете оценить результат на лету, не создавая AST.Вы можете сделать это, внедрив простой код C # в ваш файл грамматики и позволив правилам вашего синтаксического анализатора возвращать определенное значение.
Вот пример:
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"
в события перед сборкой, затем вы можете просто изменить свою грамматику и запустить проект, не беспокоясь о перестройке лексера / парсера.