本文最后更新于 2021年4月4日 晚上
入门 ANTLR 系列文章第 2 篇, 主要介绍几个简单的例子, 用于展示 ANTLR 的大部分功能:
Grammar
文件的编写入门.
- 使用
Visitor
模式编写计算器.
- 编写一个
Translator
应用, 实现从 Java Class 中抽取出 Interface.
- 直接在
Grammar
中内嵌代码的示例(针对 Listener 和 Visitor 无法实现的特殊能力).
- 其他功能, 包括 lexical 层面(token) 上的一些额外功能(比如一个文件中包含两种语言的情况).
- 如何做到忽略空白符, 但可以在后续处理中继续保留空白符信息的手段.
本章内容来源: The Definitive ANTLR 4 Reference
.
本章所有示例均采用 C#
实现, 如何生成 C# Target 详见本系列的第一篇文章.
数学表达式计算示例: 实现 ANTLR 版计算器
前提: 为保证示例足够简单, 仅实现加减乘除符号, 括号表达式, 整数和变量的支持.
完成后, 我们的语言程序将支持如下式子的计算结果输出:
1 2 3 4 5
| 193 a=5 b=6 a+b*2 (1+2)*3
|
将这些表达式保存为一个 t.expr
文件, 之后会用到.
定义 Grammar
针对上述需求定义 Grammar
文件 Expr.g4
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // grammar 名称, 必须和文件名一致 grammar Expr;
// start rule, 从这里开始解析. prog: stat+ ;
stat: expr NEWLINE | ID '=' expr NEWLINE | NEWLINE ;
expr: expr ('*'|'/') expr | expr ('+'|'-') expr | INT | ID | '(' expr ')' ;
ID : [a-zA-Z]+ ; INT : [0-9]+ ; NEWLINE: '\r'? '\n' ; WS : [ \t]+ -> skip ;
|
Grammar
由一系列 rule
构成, 这些 rule
用于描述 Language Syntax(即待处理语言的语法).
Grammar rule 包括两种:
Parser rule
: 使用小写字母开头
Lexical(token) rule
使用大写字母开头
在 Parser rule 的定义中, 使用 |
(逻辑或) 分隔该 rule 中的不同表现形式. 同时我们可以使用括号来包裹 subrule, 比如上面的 ('*'|'/')
, 它用于匹配乘号或除号.
左递归(Left-Recursive) rule:
指定义 grammar 规则的时候可以在规则定义位置引用它本身, 比如上面 expr 的第一个定义和第二个定义.
左递归的作用非常明显, 使用它就不需要对每条规则都按上下顺序书写了, 比如上面的 expr rule 定义.
在 lexical rule 定义中, 写法就和正则表达式比较类似了.
通过上面定义的 Expr.g4
Grammar 文件, 执行 antlr4 Expr.g4
进行生成操作.
然后使用 grun Expr prog -gui t.expr
测试是否正常, 结果如下图所示:
使用 grammar 文件, 生成一套 C#
版本的程序, 过程可以参考 C# target 生成一节, 只是这里生成代码时需要加上 -visitor
选项, 并且不需要 listener, 所以又添加了 -no-listener
:
1
| antlr4 -Dlanguage=CSharp -visitor -no-listener Expr.g4
|
实现计算器程序骨架
下面我们可以实现一个 C#
版本的入口程序(其他语言也类似):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| using System.IO; using Antlr4.Runtime;
namespace C4CalculatorCSharp { class Program { static void Main(string[] args) { string inputFile = ""; if (args.Length > 0) inputFile = args[0]; var txt = File.ReadAllText(inputFile); System.Console.WriteLine(txt); AntlrInputStream inputStream = new AntlrInputStream(txt); ExprLexer lexer = new ExprLexer(inputStream); CommonTokenStream tokenStream = new CommonTokenStream(lexer); ExprParser parser = new ExprParser(tokenStream); var tree = parser.prog(); var result = tree.ToStringTree(parser); System.Console.WriteLine(result); } } }
|
当执行 dotnet run ./t.expr
后(t.expr
在工程根目录), 输出如下所示:
1 2 3 4 5 6 7
| 193 a=5 b=6 a+b*2 (1+2)*3
(prog (stat (expr 193) \n) (stat a = (expr 5) \n) (stat b = (expr 6) \n) (stat (expr (expr a) + (expr (expr b) * (expr 2))) \n) (stat (expr (expr ( (expr (expr 1) + (expr 2)) )) * (expr 3)) \n))
|
Grammar 文件中的 import: 分割大型的 Grammar 文件到不同的 module (可选)
在某些场景下, 可能 Grammar 文件很大, 为了便于管理, 可以把 Grammar 的定义分割到多个独立文件中, 再通过 import
将它们组合起来.
之前的 Grammar 文件可以按下面这样分割:
lexical rule 定义文件: CommonLexerRules.g4
1 2 3 4 5 6
| lexer grammar CommonLexerRules;
ID : [a-zA-Z]+ ; INT : [0-9]+ ; NEWLINE: '\r'? '\n' ; WS : [ \t]+ -> skip ;
|
现在新的 Expr.g4
文件内容就可以是这样(为了和之前的文件区分, 这里将它命名为 LibExpr.g4
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| grammar LibExpr; import CommonLexerRules;
prog: stat+ ;
stat: expr NEWLINE | ID '=' expr NEWLINE | NEWLINE ;
expr: expr ('*'|'/') expr | expr ('+'|'-') expr | INT | ID | '(' expr ')' ;
|
只需要保证它们在同一个目录下, 再运行生成命令:
1
| antlr4 -Dlanguage=CSharp -visitor -no-listener LibExpr.g4
|
可以看到和之前的 Expr.g4
文件是同样效果.
目前仅介绍 grammar 文件的 module 分割和 import
的用法, 下面的例子中不准备分割这个文件, 因为它还比较小.
修改 Grammar 定义: 添加 Label 以支持生成多个 Visitor 入口
默认情况下, ANTLR 只会为每一个 rule 生成一个 Visitor, 而我们的语法文件中实际是在一个 rule 中通过 |
符号进行分割并定义了该 rule 的多个不同表现形式, 为了针对不同的表现形式生成不同的 Visitor, 我们就需要在 Grammar 中为每个表现形式声明 Label.
Label 声明使用 #
符号, 将 Expr.g4
文件中 parser rule 部分添加 Label, 并封装一些 subrule (仅列出修改的部分):
1 2 3 4 5 6 7 8 9 10 11
| stat: expr NEWLINE | ID '=' expr NEWLINE | NEWLINE ;
expr: expr op=('*'|'/') expr | expr op=('+'|'-') expr | INT | ID | '(' expr ')' ;
|
另外可以将 token 定义再细化, 如下所示:
1 2 3 4
| MUL : '*' ; DIV : '/' ; ADD : '+' ; SUB : '-' ;
|
修改后的完整 Expr.g4
Grammar 文件如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| // grammar 名称, 必须和文件名一致 grammar Expr;
// start rule, 从这里开始解析. prog: stat+ ;
stat: expr NEWLINE | ID '=' expr NEWLINE | NEWLINE ;
expr: expr op=('*'|'/') expr | expr op=('+'|'-') expr | INT | ID | '(' expr ')' ;
MUL : '*' ; DIV : '/' ; ADD : '+' ; SUB : '-' ; ID : [a-zA-Z]+ ; INT : [0-9]+ ; NEWLINE: '\r'? '\n' ; WS : [ \t]+ -> skip ;
|
执行 antlr4 -Dlanguage=CSharp -visitor -no-listener Expr.g4
重新生成相关文件, 可以看到在生成的 ExprBaseVisitor
类中, 包含了所有 Label 对应的 visit
方法:
1 2 3 4 5 6 7 8 9 10 11
| public partial class ExprBaseVisitor<Result> : AbstractParseTreeVisitor<Result>, IExprVisitor<Result> { public virtual Result VisitProg([NotNull] ExprParser.ProgContext context) { return VisitChildren(context); } public virtual Result VisitPrintExpr([NotNull] ExprParser.PrintExprContext context) { return VisitChildren(context); } public virtual Result VisitAssign([NotNull] ExprParser.AssignContext context) { return VisitChildren(context); } public virtual Result VisitBlank([NotNull] ExprParser.BlankContext context) { return VisitChildren(context); } public virtual Result VisitParens([NotNull] ExprParser.ParensContext context) { return VisitChildren(context); } public virtual Result VisitMulDiv([NotNull] ExprParser.MulDivContext context) { return VisitChildren(context); } public virtual Result VisitAddSub([NotNull] ExprParser.AddSubContext context) { return VisitChildren(context); } public virtual Result VisitId([NotNull] ExprParser.IdContext context) { return VisitChildren(context); } public virtual Result VisitInt([NotNull] ExprParser.IntContext context) { return VisitChildren(context); } }
|
继承 ExprBaseVisitor
类来实现 EvalVisitor
:
1 2 3 4 5 6 7 8
| namespace C4CalculatorCSharp.Visitors { public class EvalVisitor : ExprBaseVisitor<int> { } }
|
在 Main
函数中添加 Visit
调用:
1 2 3
| EvalVisitor visitor = new EvalVisitor(); visitor.Visit(tree)
|
现在整个程序就准备完成了, 下一步来看我们自定义的 EvalVisitor
内部具体应该怎么实现.
为自定义类 EvalVisitor
添加实现
如下是整体实现, 具体说明详见注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
public class EvalVisitor : ExprBaseVisitor<int> { readonly Dictionary<string, int> _memory = new Dictionary<string, int>();
public override int VisitAssign([NotNull] ExprParser.AssignContext context) { var id = context.ID().GetText(); int value = Visit(context.expr()); _memory[id] = value; return value; }
public override int VisitPrintExpr([NotNull] ExprParser.PrintExprContext context) { var value = Visit(context.expr()); System.Console.WriteLine(value); return 0; }
public override int VisitInt([NotNull] ExprParser.IntContext context) { return int.Parse(context.INT().GetText()); }
public override int VisitId(ExprParser.IdContext context) { return _memory.GetValueOrDefault(context.ID().GetText()); }
public override int VisitMulDiv([NotNull] ExprParser.MulDivContext context) { var leftVal = Visit(context.expr(0)); var rightVal = Visit(context.expr(1));
var result = context.op.Type == ExprParser.MUL ? leftVal * rightVal : leftVal / rightVal; return result; }
public override int VisitAddSub([NotNull] ExprParser.AddSubContext context) { var leftVal = Visit(context.expr(0)); var rightVal = Visit(context.expr(1)); var result = context.op.Type == ExprParser.ADD ? leftVal + rightVal : leftVal - rightVal; return result; }
public override int VisitParens([NotNull] ExprParser.ParensContext context) { return Visit(context.expr()); } }
|
再次执行 dotnet run ./t.expr
, 结果如下所示:
1 2 3 4 5 6 7 8 9
| 193 a=5 b=6 a+b*2 (1+2)*3
193 17 9
|
添加 clear 功能支持
clear 功能实际就是把自定义 visitor 中临时存储清空, 修改 grammar 定义, 添加 clear token, 并将这个 token 作为 statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| // grammar 名称, 必须和文件名一致 grammar Expr;
// start rule, 从这里开始解析. prog: stat+ ;
stat: expr NEWLINE | ID '=' expr NEWLINE | CLEAR | NEWLINE ;
expr: expr op=('*'|'/') expr | expr op=('+'|'-') expr | INT | ID | '(' expr ')' ;
CLEAR: 'clear' ; MUL : '*' ; DIV : '/' ; ADD : '+' ; SUB : '-' ; ID : [a-zA-Z]+ ; INT : [0-9]+ ; NEWLINE: '\r'? '\n' ; WS : [ \t]+ -> skip ;
|
重新生成文件: antlr4 -Dlanguage=CSharp -visitor -no-listener Expr.g4
在 visitor 中添加 clear 的处理:
1 2 3 4 5 6 7
|
public override int VisitClear(ExprParser.ClearContext context) { _memory.Clear(); return 0; }
|
将输入修改为如下(注意末尾的回车):
1 2 3 4 5 6 7 8 9 10
| 193 a=5 b=6 a+b*2 (1+2)*3
c = 12 clear 1 + c * 2
|
则输出是:
可以看到, 输出符合预期, Clear 功能实现完成.
Java Interface 转换
下面这个例子是将 Java class 转换为 interface 的例子.
假设需求是想把 Java 类转换为接口, 一般来说可以使用 java 的反射, javap, 或者是字节码库(比如 ASM 等), 但如果需要在转换的时候保留空白符号, 并保留类和方法的注释, 那这些方法就都不可用了, 只有一条路, 就是解析 Java 源码.
比如有一个类:
1 2 3 4 5
| public class Demo { void f(int x, String y) { } int[ ] g() { return null; } List<Map<String, Integer>>[] h() { return null; } }
|
我们需要转换为如下接口:
1 2 3 4 5
| interface IDemo { void f(int x, String y); int[ ] g(); List<Map<String, Integer>>[] h(); }
|
我们仍然是先写 grammar 文件, 书中已为我们提供了这个语法定义文件, 在这个链接, 在 tour 目录中找到的 Java.g4, 利用这个文件生成相关代码: antlr4 -Dlanguage=CSharp Java.g4
.
在语法文件中, 有两段是我们需要用到的:
1 2 3 4 5 6 7 8 9 10
| classDeclaration : 'class' Identifier typeParameters? ('extends' type)? ('implements' typeList)? classBody ;
methodDeclaration : type Identifier formalParameters ('[' ']')* methodDeclarationRest | 'void' Identifier formalParameters methodDeclarationRest ;
|
生成文件后, 只需要继承实现 Listener, 如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| namespace C4JavaInterfaceTranslator.Listeners { public class ExtractInterfaceListener : JavaBaseListener { private readonly JavaParser parser;
public ExtractInterfaceListener(JavaParser parser) { this.parser = parser; }
public override void EnterClassDeclaration(JavaParser.ClassDeclarationContext context) { System.Console.WriteLine($"interface I{context.Identifier()} {{"); }
public override void ExitClassDeclaration(JavaParser.ClassDeclarationContext context) { System.Console.WriteLine("}"); }
public override void EnterMethodDeclaration(JavaParser.MethodDeclarationContext context) { var tokens = parser.TokenStream; string returnType = "void"; if (context.type() != null) { returnType = tokens.GetText(context.type()); } var args = tokens.GetText(context.formalParameters()); System.Console.WriteLine($"\t{returnType} {context.Identifier()}{args}"); } } }
|
运行后, 可以看到正常输出:
1 2 3 4 5 6 7 8 9 10
| public class Demo<T> { void f(int x, String y) { } int[ ] g() { return null; } List<Map<String, Integer>>[] h() { return null; } } interface IDemo { void f(int x, String y) int[ ] g() List<Map<String, Integer>>[] h() }
|
不过要注意的是, 由于语法文件中有时会内嵌相关的处理代码, 如果和当前使用语言有冲突, 就需要修改语法定义文件, 或处理生成之后的文件.