文章目录
1. Antlr 的安装
- Step1: 下载 Antlr jar包
antlr-4.0-complete.jar - Step2: 将jar包添加至classPath
export CLASSPATH=".:/usr/local/lib/antlr-4.0-complete.jar:$CLASSPATH" - Step3: 调用antlr工具类查看是否安装成功
java -jar /usr/local/lib/antlr-4.0-complete.jar
或者 java org.antlr.v4.Tool
以上步骤是不使用集成工具的时候可使用的方法,IDEA集成了Antlr4,可在pom.xml中配置antlr相关jar包的依赖,直接使用antlr工具的功能。
2. IDEA 上Antlr插件的使用
- Step1: 安装Antlr4插件
- Step2: 创建语法文件,使用Antlr工具生成词法分析和语法分析类
语法文件ArrayInit.g4
// 语法文件通常以grammar关键字开头,语法名字必须和文件名字相匹配
grammar ArrayInit;
// 一条名为init的规则,它匹配一堆花括号中的逗号分隔的value
init : '{' value ( ',' value)* '}' ; // 必须匹配至少一个value
// 一个value可以是嵌套的花括号结构,也可以是一个简单的正数,即INT词法符号
value : init
| INT
;
// 语法分析器的规则必须以小写字母开头,词法分析器的规则必须用大写字母开头
INT : [0-9]+ ;
WS : [ \t\r\n]+ -> skip ; // 定义词法规则"空白符号",丢弃
设置词法、语法分析器生成目录和包名:右击文件名,选中 Configure ANTLR…进行设置
- Step3: 右击文件名,选中 Generate ANTLR Recognizer…在设置的位置下会生成一些列java类
- Step4: 添加依赖
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.7.2</version>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
<version>4.7.2</version>
</dependency>
- Step5: ANTLR Preview的使用
左侧输入要测试的语句,选中语法文件中的语法规则右击,选择 Test Rule xx, 在右边会生成相应的语法树
3. 使用Antlr工具生成的文件
- ArrayInitParser.java: 语法分析器类的定义,专门用来识别我们定义的"数组语言"的语法。每条规则都有对应的方法。
- ArrayInitLexer.java: 词法分析器的类定义,由ANTLR通过分析词法规则INT和WS,以及语法中的字面值’{’, ‘,’ , '}'生成的。作用是将输入字符序列分解成词汇符号。
- ArrayInit.tokens: ANTLR会给我们定义的每个词法符号指定一个数字形式的类型,然后将映射关系存储在这个文件中。
- ArrayInitListener.java, ArrayInitBaseListener.java:在遍历语法分析树时,遍历器能够触发一系列事件(回调),并通知我们提供的监听对象。ArrayInitListener接口给出了这些回调方法的定义,我们可以实现它来完成自定义功能,ArrayInitBaseListener是该接口的默认实现类(空实现)。
4. Antlr 使用案例
4.1 使用步骤
- Step1: 创建语法文件,语法文件必须为 .g4文件
- Step2: 使用ANTLR工具生成java类等文件
- Step3: 按照需求将生成的类集成到更大的应用程序中
Step1 和 Step2上面已经给出步骤,下面主要介绍 Step3 中不同场景的使用方式
4.2 将生成的语法分析器与JAVA程序集成
public class TestArrayInit {
public static void main(String[] args) throws Exception {
// 1. 获取字符输入流 CharStream
ANTLRInputStream input = new ANTLRInputStream(System.in);
// 2. 创建词法分析器
ArrayInitLexer lexer = new ArrayInitLexer(input);
// 3. 创建词法符号缓冲区,存储由词法分析器生成的词法符号
CommonTokenStream tokens = new CommonTokenStream(lexer);
// 4. 创建语法分析器
ArrayInitParser parser = new ArrayInitParser(tokens);
// 5. 生成语法分析树
ParseTree tree = parser.init();
// 6. 打印语法分析树
System.out.println(tree.toStringTree(parser));
}
}
上述代码仅仅能够生成语法树,或者检查语法正确性(ANTLR能够自动报告语法错误)。我们还需要对输入的语句进行另外的处理,例如能够将short数组初始化语句转换为String对象,这时候就可以使用ANTLR内置的语法分析树遍历器进行深度优先遍历,然后在它触发的一系列回调函数中进行适当的操作。
4.3 使用监听器操纵输入的数据
在语法规则对应的语句的开始和结束位置处,监听器方法可以得到通知,所以我们只要继承监听器,然后实现我们感兴趣的方法。
例如short数组 {99, 3, 451}
翻译成 String形式 “\u0063 \u0003 \u01c3”
翻译过程就是:
将 { 翻译为 "
将 } 翻译为 "
将每个整数翻译为四位的十六进制形式然后加前缀 \u
遵循我们的翻译规则的监听器实现类为:
public class ShortToUnicodeString extends ArrayInitBaseListener{
// 将 { 翻译为 "
@Override
public void enterInit(ArrayInitParser.InitContext ctx) {
System.out.println('"');
}
// 将 } 翻译为 "
@Override
public void exitInit(ArrayInitParser.InitContext ctx) {
System.out.println('"');
}
// 将每个整数翻译为4位的十六进制形式,然后加前缀 \u
@Override
public void enterValue(ArrayInitParser.ValueContext ctx) {
// 假定不存在嵌套结构
int value = Integer.valueOf(ctx.INT().getText());
System.out.printf("\\u%04x", value);
}
}
实现完监听器后,我们要应用于主程序
主程序中 Step1 ~ Step5 与 4.2 相同,监听器在主程序中的使用如下
// 7. 新建一个通用的、能够触发回调函数的语法分析遍历器
ParseTreeWalker walker = new ParseTreeWalker();
// 8. 遍历语法分析过程中生成语法分析树,触发回调
walker.walk(new ShortToUnicodeString(), tree);
4.4 增强版语法
案例:计算器的实现(只允许基本的算数操作符: 加减乘除、圆括号、整数以及变量)
本语言的全部特性:一个语句可以是一个表达式、一个赋值语句或者是一个空行
193
a = 5
b = 6
a + b*2
(1+2)*3
ANTLR语法:
grammar Expr;
// 起始规则,语法分析的七点
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;
以上案例中语法都比较简单,但是语法比较复杂,有成千上万条的情况下,我们要如何处理呢?
跟在软件开发中一样,我们可以将语法拆分成逻辑单元,然后采用语法导入的方式将它们结合起来。
例如:将上面语法拆分成语法分析器的语法和词法分析器的语法
词法规则:
lexer grammar CommonLexerRules; // 注意是 lexer grammar
ID : [a-zA-Z]+ ;
INT: [0-9]+ ;
NEWLINE:'\r'?'\n';
WS : [ \t]+ -> skip ;
语法规则:
grammar LibExpr;
import CommonLexerRules;
prog: stat+ ;
stat: expr NEWLINE
| ID '=' expr NEWLINE
| NEWLINE
;
expr: expr ('*'|'/') expr
| expr ('+'|'-') expr
| INT
| ID
| '(' expr ')'
;
一般情况下,在ANTLR自带的遍历器(监听器和访问器)中,ANTLR会为每条规则生成一个方法,但是在规则下有很多备选分支的情况下,很多情况下我们会希望为每个备选分支生成不同的方法,这样对每种输入就都可以有不同的实现。这时候我们可以为每个备选分支加上标签,语法修改如下:
grammar LibExpr;
import CommonLexerRules;
prog: stat+ ;
stat: expr NEWLINE # printExpr
| ID '=' expr NEWLINE # assign
| NEWLINE # blank
;
expr: expr op=('*'|'/') expr # MulDiv
| expr op=('+'|'-') expr # AddSub
| INT # int
| ID # id
| '(' expr ')' # parens
;
// 为运算符这样词法符号定义一些名字
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
4.5 访问器的使用
写一个类继承ANTLR自动生成的空访问器接口实现类,重写其中我们感兴趣的方法:
package demo.calculate;
import java.util.HashMap;
import java.util.Map;
public class EvalVisitor extends LibExprBaseVisitor<Integer> {
// 存放变量名和变量值的对应关系
Map<String, Integer> memory = new HashMap<String, Integer>();
// ID '=' expr NEWLINE
@Override
public Integer visitAssign(LibExprParser.AssignContext ctx) {
String id = ctx.ID().getText();
int value = visit(ctx.expr());
memory.put(id, value);
return value;
}
// expr NEWLINE
@Override
public Integer visitPrintExpr(LibExprParser.PrintExprContext ctx) {
Integer value = visit(ctx.expr());
System.out.println(value);
return 0; // 返回一个假值
}
// INT
@Override
public Integer visitInt(LibExprParser.IntContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}
// ID
@Override
public Integer visitId(LibExprParser.IdContext ctx) {
String id = ctx.ID().getText();
return memory.containsKey(id) ? memory.get(id) : 0;
}
// expr ('*'|'/') expr
@Override
public Integer visitMulDiv(LibExprParser.MulDivContext ctx) {
int left = visit(ctx.expr(0));
int right = visit(ctx.expr(1));
return ctx.op.getType() == LibExprParser.MUL ? left*right : left/right;
}
// expr op=('+'|'-') expr
@Override
public Integer visitAddSub(LibExprParser.AddSubContext ctx) {
int left = visit(ctx.expr(0));
int right = visit(ctx.expr(1));
return ctx.op.getType() == LibExprParser.ADD ? left + right : left - right;
}
// '(' expr ')'
@Override
public Integer visitParens(LibExprParser.ParensContext ctx) {
return visit(ctx.expr());
}
}
4.6 在语法中嵌入动作
案例:从文本文件中提取特定的列
语法文件如下:
grammar Rows;
// 自定义构造器以便能传入希望提取的列号(从1开始计数)
@parser::members { // 在生成的RowsParser中添加一些成员
int col;
public RowsParser(TokenStream input, int col) {
this(input);
this.col = col;
}
}
// 匹配输入文件的语法
file : (row NL)+ ;
row
locals [int i=0]
: ( STUFF
{
$i++;
if ( $i == col ) System.out.println($STUFF.text);
}
)+
;
TAB : '\t' -> skip ; // 匹配但是不将其传递给语法分析器
NL : '\r'?'\n' ; // 匹配并将其传递给语法分析器
STUFF : ~[\t\r\n]+ ; // 匹配除tab符合换行符之外的任何字符
java整合代码如下:
ANTLRInputStream input = new ANTLRInputStream(System.in);
RowsLexer lexer = new RowsLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
int col = Integer.valueOf(args[0]);
RowsParser parser = new RowsParser(tokens, col); // 传递序列号作为参数
parser.setBuildParseTree(false); // 不需要浪费时间建立语法分析树
parser.file(); // 开始语法分析
5. 小结
以上主要通过案例对如何使用ANTLR工具进行了简要的说明,接下来对语法文件定义的具体规则进行具体学习。