编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。

简介

ANTLR 4 是一款强大的语法生成器工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。它是目前 Java 生态中使用最为广泛的语法生成器工具之一。众多知名项目与企业均采用了 ANTLR 进行语法分析,例如:

  • Twitter 搜索:每天处理超过 20 亿次查询。
  • Hadoop 生态系统:Hive、Pig 等数据仓库和分析系统所使用的语言。
  • Lex Machina:用于分析法律文本。
  • Oracle:SQL 开发者 IDE 和迁移工具。
  • NetBeans IDE:用于解析 C++ 代码。
  • Hibernate:对象 - 关系映射框架(ORM)用于处理 HQL 语言。

ANTLR 4 提供了大量的 官方 grammar 示例,涵盖了各种常见语言,资源非常全面,提供了丰富的学习教材。

本文将通过一个简单的计算器示例,介绍 ANTLR 4 的基本用法。这个例子在很多资料中都作为入门示例,堪称 ANTLR 4 的 "Hello World"。

基本概念

语法分析器(Parser)是用来识别语言的程序,通常包含两个核心部分:词法分析器(Lexer)语法分析器(Parser)

  • 词法分析阶段:主要解决关键词以及各种标识符的识别,例如 INTID 等。
  • 语法分析阶段:基于词法分析的结果,构造一棵语法分析树(Parse Tree)。

大致的流程如下图所示(参考文档 2):

语法解析基本流程

因此,为了让词法分析和语法分析能够正常工作,在使用 ANTLR 4 的时候,需要定义语法(Grammar),这部分使用的是 ANTLR 元语言。

语法解析基本流程

元语言

首先,要了解 ANTLR 4 本身定义 Grammar 的语法,相对比较简单。我们以计算器的例子为例,简单讲解其中的概念。

// file: Calculator.g4
grammar Calculator;

line : expr EOF ;
expr : '(' expr ')'             # parenExpr
     | expr ('*'|'/') expr      # multOrDiv
     | expr ('+'|'-') expr      # addOrSubstract
     | FLOAT                    # float
     ;

WS : [ \t\n\r]+ -> skip;
FLOAT : DIGIT+ '.' DIGIT* EXPONET?
      | '.' DIGIT+ EXPONET?
      | DIGIT+ EXPONET?
      ;

fragment DIGIT : '0'..'9' ;
fragment EXPONET : ('e'|'E') ('+'|'-')? DIGIT+ ;
  • 第一行:定义了 Grammar 的名字,名字需要与文件名对应。
  • 语法规则:接下来的 lineexpr 就是定义的语法,会使用到下方定义的词法。注意 # 后面的名字(如 # parenExpr),是可以在后续访问和处理的时候使用的标签(Label)。一个语法有多种规则的时候可以使用 | 来进行配置。
  • 优先级:在 expr 这行,我们注意到四则运算分为了两个非常相似的语句,这样做的原因是为了实现优先级,乘除的优先级高于加减。
  • 空白字符WS 定义了空白字符,后面的 skip 是一个特殊的标记,表示空白字符会被忽略。
  • 浮点数FLOAT 是定义的浮点数,包含了整数,与编程语言中的浮点数略有不同,更类似 Number 的定义。
  • 片段规则:最后的 fragment 定义了两个在词法定义中使用到的符号,它们不会被单独生成 Token,仅供其他规则引用。

在语法定义的文件中,大部分的地方使用了正则表达式。

生成代码

配置 ANTLR 4 工具,先从官网下载 ANTLR 4 的 Jar 包,点击 下载地址 进行下载。

alias antlr4="java -jar /path/to/antlr-4.5-complete.jar"

通过命令行工具可以生成 Lexer、Parser、Visitor、Listener 等文件。

注意:Visitor 是默认不生成的,需要带上参数 -visitor
$ antlr4 -visitor Calculator.g4

# 生成文件如下:
Calculator.interp
CalculatorBaseListener.java
CalculatorLexer.interp
CalculatorLexer.tokens
CalculatorParser.java
Calculator.tokens
CalculatorBaseVisitor.java
CalculatorLexer.java
CalculatorListener.java
CalculatorVisitor.java

使用 Visitor 模式

Visitor 的使用是最为简单方便的,继承 CalculatorBaseVisitor 类即可,内部的方法与 .g4 文件定义相对应,对照即可理解。

public class MyCalculatorVisitor extends CalculatorBaseVisitor<Object> {
    @Override
    public Object visitParenExpr(CalculatorParser.ParenExprContext ctx) {
        return visit(ctx.expr());
    }

    @Override
    public Object visitMultOrDiv(CalculatorParser.MultOrDivContext ctx) {
        Object obj0 = ctx.expr(0).accept(this);
        Object obj1 = ctx.expr(1).accept(this);

        if ("*".equals(ctx.getChild(1).getText())) {
            return (Float) obj0 * (Float) obj1;
        } else if ("/".equals(ctx.getChild(1).getText())) {
            return (Float) obj0 / (Float) obj1;
        }
        return 0f;
    }

    @Override
    public Object visitAddOrSubstract(CalculatorParser.AddOrSubstractContext ctx) {
        Object obj0 = ctx.expr(0).accept(this);
        Object obj1 = ctx.expr(1).accept(this);

        if ("+".equals(ctx.getChild(1).getText())) {
            return (Float) obj0 + (Float) obj1;
        } else if ("-".equals(ctx.getChild(1).getText())) {
            return (Float) obj0 - (Float) obj1;
        }
        return 0f;
    }

    @Override
    public Object visitFloat(CalculatorParser.FloatContext ctx) {
        return Float.parseFloat(ctx.getText());
    }
}

实现了 Visitor 之后,就可以完成一个简单的计算器了。

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;

public class Driver {
    public static void main(String[] args) {
        String query = "3.1 * (6.3 - 4.51) + 5 * 4";

        CalculatorLexer lexer = new CalculatorLexer(new ANTLRInputStream(query));
        CalculatorParser parser = new CalculatorParser(new CommonTokenStream(lexer));
        CalculatorVisitor visitor = new MyCalculatorVisitor();

        System.out.println(visitor.visit(parser.expr()));  // 25.549
    }
}

集成 Maven

ANTLR 4 提供了 Maven Plugin,可以通过配置来进行编译。

语法文件 .g4 放置在 src/main/antlr4 目录下即可,配置依赖的 antlr4plugin 即可。

生成 Visitor 在 Plugin 配置 visitor 参数为 true 即可。

注意:ANTLR 4 的库版本要与 Plugin 版本对应,ANTLR 4 对生成文件用的版本与库本身的版本会进行对照,不匹配会报错。
...
<properties>
    <antlr4.version>4.7.2</antlr4.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4</artifactId>
        <version>${antlr4.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.antlr</groupId>
            <artifactId>antlr4-maven-plugin</artifactId>
            <version>${antlr4.version}</version>
            <configuration>
                <visitor>true</visitor>
            </configuration>
            <executions>
                <execution>
                    <id>antlr</id>
                    <goals>
                        <goal>antlr4</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
...

总结

本文较为简单地说了一个计算器的例子,只介绍了 Visitor 的大致使用方法,最后还介绍了 Maven Plugin 的使用方法。

笔者还在学习 ANTLR 4 以及编译原理,想用好语法生成器工具,还是需要对编译原理有一定的理解。

本文的计算器实现代码可以在 这里 上找到。

参考文档

  1. antlr
  2. ANTLR 4 权威指南
  3. ANTLR 快餐教程 (2) - ANTLR 其实很简单

版本说明

  • 工具版本:文中命令行示例使用的是 ANTLR 4.5 版本 Jar 包,而 Maven 配置示例使用的是 4.7.2 版本,实际使用时请保持版本一致。
  • API 变更:代码示例中使用的 ANTLRInputStream 在较新版本中已标记为 deprecated,建议在新项目中查阅官方文档使用 CharStream 相关 API。
  • 时效性:外部链接与图片资源可能随时间失效,请以官方最新文档为准。