牛刀小试
antlr4 的安装
上一章节我们简单介绍了一下 antlr4,这一章节,开始讨论 antlr4 的使用和文法。首先简单介绍一下 antlr4 工具的安装和使用参数,非常简单。
-
安装 java 1.7 及以上版本,配置 java 环境变量
-
下载 antlr4 工具
https://www.antlr.org/download/antlr-4.9.2-complete.jar
,可以将 antlr4 工具添加到CLASSPATH
环境变量中,比如添加到.bash_profile
或者.bashrc
文件中export CLASSPATH=".:/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH"
-
创建 antlr4 工具的别名
alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH" org.antlr.v4.Tool' alias grun='java -Xmx500M -cp "/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH" org.antlr.v4.gui.TestRig'
温馨提示:antlr4 本身就是 java 来实现的,面向 java 语言的示例非常多,笔者写作的过程中主要使用的是 c++ 语言,后续的所有示例也几乎都是 c++ 语言来实现的。
上述安装完成之后,输入 antlr4,在 terminal 上会出现如下信息
ANTLR Parser Generator Version 4.7.1
-o ___ specify output directory where all output is generated
-lib ___ specify location of grammars, tokens files
-atn generate rule augmented transition network diagrams
-encoding ___ specify grammar file encoding; e.g., euc-jp
-message-format ___ specify output style for messages in antlr, gnu, vs2005
-long-messages show exception details when available for errors and warnings
-listener generate parse tree listener (default)
-no-listener don't generate parse tree listener
-visitor generate parse tree visitor
-no-visitor don't generate parse tree visitor (default)
-package ___ specify a package/namespace for the generated code
-depend generate file dependencies
-D<option>=value set/override a grammar-level option
-Werror treat warnings as errors
-XdbgST launch StringTemplate visualizer on generated code
-XdbgSTWait wait for STViz to close before continuing
-Xforce-atn use the ATN simulator for all predictions
-Xlog dump lots of logging info to antlr-timestamp.log
-Xexact-output-dir all output goes into -o dir regardless of paths/package
列出几个常用的比较重要的选项进行说明
- -o 选项指定语法分析器生成的目录
- -lib 目录制定语法,tokens 文件的位置
- -encoding 制定语法文件的编码
- -long-messages 现实详细的错误或者告警信息,通常可以配合与 -Werror 一起使用
- -listener 生成监听器接口,默认选项
- -no-listener 不生成监听器接口
- -visitor 生成访问器接口
- -no-visitor 不生成访问器接口,默认选项
- -package 指定生成代码的命名空间,比如 c++ 代码,这就是指定了语法生成器的命名空间 namespace
一个简单识别表达式的例子
我们使用一个简单的例子来展示一下 antlr4 的使用方法和使用效果,语法 2-1 如下所示
grammar Math;
compileUnit
: expr EOF
;
expr
: '(' expr ')' # parenExpr
| op=(ADD | SUB) expr # unaryExpr
| left=expr op=(MUL | DIV) right=expr # mulDivExpr
| left=expr op=(ADD | SUB) right=expr # addSubExpr
| func=ID '(' expr (',' expr)? ')' # funcExpr
| NUM # numExpr
;
ADD : '+';
SUB : '-';
MUL : '*';
DIV : '/';
ID : [a-zA-Z]+ ;
NUM : [0-9]+ ('.' [0-9]+)? ([eE] [+-]? [0-9]+)? ;
WS : [ \t\r\n] -> channel(HIDDEN) ;
这是一段识别简单表达式——加减乘除表达式——的语法。expr 代表表达式,而表达式本身可以是括号括起来的表达式,也可以是加减乘除表达式,还可以是函数调用,数字同样也是表达式,这里就应用到了递归的思想。下面我们来看一下解析的效果。
我们通过 antlr 工具,生成表达式的语法分析器
java -Xmx500M -cp antlr-4.9.2-complete.jar org.antlr.v4.Tool -Dlanguage=Cpp -listener -visitor -o generated/ Math.g4
笔者选择的目标语言是 c++ 语言,所以 antlr4 生成语法分析器是针对 cpp 的。执行上述操作后,在当前目录的 generated 文件夹下会生成如下文件
MathBaseListener.cpp MathBaseVisitor.cpp Math.interp MathLexer.h MathLexer.tokens MathListener.h MathParser.h MathVisitor.cpp
MathBaseListener.h MathBaseVisitor.h MathLexer.cpp MathLexer.interp MathListener.cpp MathParser.cpp Math.tokens MathVisitor.h
生成了监听器接口,访问器接口,因为没有使用 -package
选项,所以没有指定命名空间。下面我们使用一个简单的 main 方法来调用这些接口,代码 2-1 如下所示
int main(int argc, char *argv[]) {
ANTLRInputStream input(argv[1]);
MathLexer lexer(&input);
CommonTokenStream tokens(&lexer);
// show all the tokens
tokens.fill();
for (auto token : tokens.getTokens()) {
std::cout << token->toString() << std::endl;
}
MathParser parser(&tokens);
auto compile_unit_ctx = parser.compileUnit();
std::cout << "text: " << tokens.getText() << std::endl;
std::cout << ((tree::ParseTree*)(compile_unit_ctx))->toStringTree(&parser) << std::endl;
return 0;
}
main 方法中,将命令行参数的第一个参数作为表达式输入,创建 antlr 输入流对象,通过输入流构造 MathLexer 词法对象,进行词法解析,词法将输入流解析成 token 流,将 token 流传递给语法分析器 MathParser 进行语法解析,语法解析结果就是一棵 Parse tree 的树型结构。语法生成器提供了监听器和访问器两种方式对树形结构进行访问。上面的代码,在语法解析器成功解析的情况下,将 token 标记和 Parse tree 都打印出来了,我们尝试对如下的表达式进行解析
10-3/(8-1*4)
结果输出如下
[@0,0:1='10',<9>,1:0]
[@1,2:2='-',<5>,1:2]
[@2,3:3='3',<9>,1:3]
[@3,4:4='/',<7>,1:4]
[@4,5:5='(',<1>,1:5]
[@5,6:6='8',<9>,1:6]
[@6,7:7='-',<5>,1:7]
[@7,8:8='1',<9>,1:8]
[@8,9:9='*',<6>,1:9]
[@9,10:10='4',<9>,1:10]
[@10,11:11=')',<2>,1:11]
[@11,12:11='<EOF>',<-1>,1:12]
text: 10-3/(8-1*4)
(compileUnit (expr (expr 10) - (expr (expr 3) / (expr ( (expr (expr 8) - (expr (expr 1) * (expr 4))) )))) <EOF>)
@0 表示第0个位置(从0开始), 0:1 表明在第0-1个字符之间,内容是 ‘10’,token 的 id 是 9, 1:0 表示的是,位于输入字符串第一行,第0个位置处。那么这个 token 的 id 是哪来的呢,这个可以从上面生成的文件 MathLexer.tokens
文件中查看
T__0=1
T__1=2
T__2=3
ADD=4
SUB=5
MUL=6
DIV=7
ID=8
NUM=9
WS=10
'('=1
')'=2
','=3
'+'=4
'-'=5
'*'=6
'/'=7
id 是 9 说明这个 token 是数字 NUM,而
(compileUnit (expr (expr 10) - (expr (expr 3) / (expr ( (expr (expr 8) - (expr (expr 1) * (expr 4))) )))) <EOF>)
就是语法解析的完整的树形结构,把这棵树整理一下,如图 2-1 所示