语法解析器ANTLR4简易教程

Antlr (ANother Tool for Language Recognition) 是一个强大的跨语言语法解析器,可以用来读取、处理、执行或翻译结构化文本或二进制文件。 它被广泛用来构建语言,工具和框架。。ANTLR 根据语法定义生成解析器,解析器可以构建和遍历解析树。

所有编程语言的语法,都可以用ANTLR来定义。ANTLR提供了大量的官方 grammar 示例, 包含了各种常见语言,比如Java、SQL、Javascript、PHP等等。

谁在使用

  • Twitter搜索使用ANTLR进行语法分析,每天处理超过20亿次查询;
  • Hadoop生态系统中的Hive、Pig、数据仓库和分析系统所使用的语言都用到了ANTLR;
  • Lex Machina将ANTLR用于分析法律文本;Oracle公司在SQL开发者IDE和迁移工具中使用了ANTLR;
  • NetBeans公司的IDE使用ANTLR来解析C++;
  • Hibernate对象-关系映射框架(ORM)使用ANTLR来处理HQL语言
  • 其他还有Oracle、Presto、Elasticsearch、Spark

官网地址:https://www.antlr.org

基本概念

img.png

  1. 词法分析器 (Lexer)词法分析是指在计算机科学中,将字符序列转换为单词(Token)的过程。词法分析器(Lexer)一般是用来供语法解析器(Parser)调用的。
  2. 语法解析器 (Parser) 语法解析器通常作为编译器或解释器出现。它的作用是进行语法检查,并构建由输入单词(Token)组成的数据结构(即抽象语法树)。 语法解析器通常使用词法分析器(Lexer)从输入字符流中分离出一个个的单词(Token),并将单词(Token)流作为其输入。实际开发中,语法解析器可以手工编写,也可以使用工具自动生成。
  3. 抽象语法树 (Abstract Syntax Tree,AST) 抽象语法树是源代码结构的一种抽象表示,它以树的形状表示语言的语法结构。抽象语法树一般可以用来进行代码语法的检查, 代码风格的检查,代码的格式化,代码的高亮,代码的错误提示以及代码的自动补全等等。

其他常见的语法分析器

JavaCC

JavaCC,即Java Cmopiler Compiler,为了简化基于Java语言的词法分析器或者语法分析器的开发,Sun公司的开发人员开发了JavaCC(Java Compiler Compiler)。 JavaCC是一个基于Java语言的分析器的自动生成器。用户只要按照JavaCC的语法规范编写JavaCC的源文件,然后使用JavaCC的编译器编译, 就能够生成基于Java语言的某种特定语言的分析器。JavaCC已经成为最受欢迎的Java解析器创建工具。

YACC

YACC(Yet Another Compiler-Compiler): 1975 年由贝尔实验室 Mike Lesk & Eric Schmidt 开发,UNIX 标准实用工具 (utility)。

SqlParser

位于Alibaba的Druid库中,只能解析sql语句,功能比较单一。

安装方式

有多种安装Antlr4的方式:

  1. 使用pip方式安装antlr4-tools
  2. Maven工程中引入antlr4的依赖
  3. IDEA中安装antlr4的插件

我平时都是喜欢用maven开发代码,因此选择第二种maven方式。

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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xncoding</groupId>
<artifactId>antlr4-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<name>antlr4-demo</name>
<description>antlr4 java source</description>
<url>https://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>

<build>
<finalName>antlr4</finalName>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>4.13.2</version>
<executions>
<execution>
<id>antlr4</id>
<goals>
<goal>antlr4</goal>
</goals>
<configuration>
<includes>
<include>**/*.g4</include>
</includes>
<listener>true</listener>
<visitor>true</visitor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

基本使用步骤

通过一个简单额hello world的例子来介绍Antlr4语法解析器开发的基本使用步骤。

定义一个G4文件

需要创建一个.g4文件,用于定义词法分析器(lexer)和语法解析器(Parser)。具体语法参见官方文档。下面是一个简单的例子:Hello.g4。

1
2
3
4
5
6
7
// file Hello.g4
// Define a grammar called Hello
grammar Hello; // 1. grammer name
@header { package pers.me.expression.parser; } // 2. java package
r : 'hello' ID ; // 3. match keyword hello followed by an identifier
ID : [a-z]+ ; // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // 4. skip spaces, tabs, newlines
  1. 定义了 grammar 的名字,名字需要与文件名对应
  2. 定义生成的Java类的package
  3. r 定义的语法,会使用到下方定义的正则表达式词法
  4. 定义了空白字符,后面的 skip 是一个特殊的标记,标记空白字符会被忽略

生成代码

执行 mvn clean compile 可自动生成Antlr的代码。

测试

我们可以利用下面这段代码来测试一下ParseTree。

1
2
3
4
5
6
7
8
9
public class HelloTest {
public static void main(String[] args) throws Exception {
HelloLexer lexer = new HelloLexer(CharStreams.fromString("hello world"));
CommonTokenStream tokens = new CommonTokenStream(lexer);
HelloParser parser = new HelloParser(tokens);
ParseTree tree = parser.r();
System.out.println(tree.toStringTree(parser));
}
}

运行上面的代码可以得到如下输出,程序识别出输入的字符串符合r的语法。

1
(r hello world)

Listener和Visitor

ANTLR提供了两种方法来访问ParseTree:

  1. 一种是通过Parse-Tree Listener的方法
  2. 另一种是通过Parse-Tree Visitor的方法

Listener有以下特点:

  1. 访问AST的所有节点
  2. 重写(Override)进入时(enterXXX方法)和退出时(exitXXX方法)要执行的方法
  3. 要重写的方法没有返回值,因此需要在属性中保留所需的值

Visitor有以下特点:

  1. 并非所有 AST 节点都被访问
  2. 根据目的重写进入节点时要执行的过程(visitXXX方法)
  3. 重写方法有一个返回值,因此您不必在属性中保存所需的值

最大的区别是Listener会自动访问 AST 的所有节点,而Visitor如果要访问当前节点的子树,则需要手工实现。 Visitor 较为简单方便,继承 HelloBaseVisitor 类即可,内部的方法与 g4 文件定义相对应,对照看即可理解。实现了 visitor 之后, 就可以完成一个简单的自定义解析器了。自动生成的HelloBaseVisitor.java如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor;
/**
* This class provides an empty implementation of {@link HelloVisitor},
* which can be extended to create a visitor which only needs to handle a subset
* of the available methods.
*
* @param <T> The return type of the visit operation. Use {@link Void} for
* operations with no return type.
*/
public class HelloBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements HelloVisitor<T> {
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitR(HelloParser.RContext ctx) { return visitChildren(ctx); }
}

计算器的实现

接下来进入实战环节,我们来实现一个简单的计算器。

定义语法和词法

创建Expression.g4。

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
// 定义了 grammar 的名字,名字需要与文件名对应
grammar Expression;
@header {
// 定义package
package pers.me.expression.parser;
}

/**
* parser
* calc 和 expr 就是定义的语法,会使用到下方定义的词法
* 注意 # 后面的名字,是可以在后续访问和处理的时候使用的。
* 一个语法有多种规则的时候可以使用 | 来进行配置。
*/

calc
: (expr)* EOF # calculationBlock
;
// 四则运算分为了两个非常相似的语句,这样做的原因是为了实现优先级,乘除是优先级高于加减的。
expr
: BR_OPEN expr BR_CLOSE # expressionWithBr
| sign=(PLUS|MINUS)? num=(NUMBER|PERCENT_NUMBER) # expressionNumeric
| expr op=(TIMES | DIVIDE) expr # expressionTimesOrDivide
| expr op=(PLUS | MINUS) expr # expressionPlusOrMinus
;

/**
* lexer
*/

BR_OPEN: '(';
BR_CLOSE: ')';
PLUS: '+';
MINUS: '-';
TIMES: '*';
DIVIDE: '/';
PERCENT: '%';
POINT: '.';

// 定义百分数
PERCENT_NUMBER
: NUMBER ((' ')? PERCENT)
;

NUMBER
: DIGIT+ ( POINT DIGIT+ )?
;

DIGIT
: [0-9]+
;

// 定义了空白字符,后面的 skip 是一个特殊的标记,标记空白字符会被忽略
SPACE
: ' ' -> skip
;

实现Visitor

生成Java文件之后,我们来实现自己的Visitor,用于支持BigDecimal。

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
public class BigDecimalCalculationVisitor extends ExpressionBaseVisitor<BigDecimal> {

/**
* 100
*/
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);

/**
* DECIMAL128のMathContext (桁数34、RoundingMode.HALF_EVEN)
*/
private static final MathContext MATH_CONTEXT = MathContext.DECIMAL128;

@Override
public BigDecimal visitCalculationBlock(ExpressionParser.CalculationBlockContext ctx) {
BigDecimal calcResult = null;
for (ExpressionParser.ExprContext expr : ctx.expr()) {
calcResult = visit(expr);
}
return calcResult;
}

@Override
public BigDecimal visitExpressionTimesOrDivide(ExpressionParser.ExpressionTimesOrDivideContext ctx) {
BigDecimal left = visit(ctx.expr(0));
BigDecimal right = visit(ctx.expr(1));
switch (ctx.op.getType()) {
case ExpressionLexer.TIMES: return left.multiply(right, MATH_CONTEXT);
case ExpressionLexer.DIVIDE: return left.divide(right, MATH_CONTEXT);
default: throw new RuntimeException("unsupported operator type");
}
}

@Override
public BigDecimal visitExpressionPlusOrMinus(ExpressionParser.ExpressionPlusOrMinusContext ctx) {
// 此处加减法的实现类似上面的乘除法,省略
}

@Override
public BigDecimal visitExpressionWithBr(ExpressionParser.ExpressionWithBrContext ctx) {
return visit(ctx.expr());
}

@Override
public BigDecimal visitExpressionNumeric(ExpressionParser.ExpressionNumericContext ctx) {
BigDecimal numeric = numberOrPercent(ctx.num);
if (Objects.nonNull(ctx.sign) && ctx.sign.getType() == ExpressionLexer.MINUS) {
return numeric.negate();
}
return numeric;
}

private BigDecimal numberOrPercent(Token num) {
String numberStr = num.getText();
switch (num.getType()) {
case ExpressionLexer.NUMBER: return decimal(numberStr);
case ExpressionLexer.PERCENT_NUMBER: return decimal(numberStr.substring(0, numberStr.length() - 1).trim()).divide(HUNDRED, MATH_CONTEXT);
default: throw new RuntimeException("unsupported number type");
}
}

private BigDecimal decimal(String decimalStr) {
return new BigDecimal(decimalStr);
}

}

调用解析器

在Calculator类中调用Expression。

1
2
3
4
5
6
7
8
9
10
11
public class Calculator {
public BigDecimal execute(String expression) {
CharStream cs = CharStreams.fromString(expression);
ExpressionLexer lexer = new ExpressionLexer(cs);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExpressionParser parser = new ExpressionParser(tokens);
ExpressionParser.CalcContext context = parser.calc();
BigDecimalCalculationVisitor visitor = new BigDecimalCalculationVisitor();
return visitor.visit(context);
}
}

测试

最后,我们用一个jUnit来测试一下我们的计算器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CalculatorUnitTest {

private final Calculator calculator = new Calculator();

@DisplayName("Test Calculator")
@ParameterizedTest
@CsvSource({
"1 + 2, 3",
"3 - 2, 1",
"2 * 3, 6",
"6 / 3, 2",
"6 / (1 + 2) , 2",
"50%, 0.5",
"100 * 30%, 30.0"
})
void testCalculation(String expression, String expected) {
assertEquals(expected, calculator.execute(expression).toPlainString());
}

}

推荐阅读

  1. Antlr4官方指南
  2. Antlr4官方示例:Grammars-v4