编译原理教程(二)了解Antlr4的使用

Antlr 是什么

Antlr 是一个开源的工具,支持根据规则文件生成词法分析器和语法分析器。Antlr 支持很多的目标语言,包括 Java、C#、JavaScript、Python、Go、C++、Swift等。无论你用上面哪种语言,都可以用它生成词法和语法分析的功能。同时,相对于其他的前端工具, Antlr 的语法更加简单。它能把类似左递归的一些常见难点在工具中解决,对提升工作效率有很大的帮助。

安装 Antlr4

Windows上的安装流程如下所示,其他系统的安装教程见官方文档

第一步:我们需要使用下面的命令安装 antlr4-tools

shell 复制代码
pip install antlr4-tools

第二步,安装完 antlr4-tools 后执行下面命令,它会配置好相应的依赖和命令。

shell 复制代码
antlr4

第三步,配置 CLASSPATH 环境变量,如下所示。配置完成后,记得重启命令行。

shell 复制代码
.;F:\antlr\antlr-4.13.2-complete.jar;C:\Program Files\Java\jdk-17\lib;

第四步,创建一个 Calculate.g4 规则文件,该文件在Antlr中是用来生成词法分析器和语法分析器的,内容如下所示(该文件的内容后面介绍,这里不需要看懂):

g4 复制代码
grammar Calculate;

@header {
package example.calculate;
}

prog: expr EOF;

expr:  expr ('*'|'/') expr   # MulDiv
     | expr ('+'|'-') expr   # AddSub
     | INT                    # int
     | '(' expr ')'          # parens
     ;

INT:[0-9]+;
MUL: '*';
DIV: '/';
ADD: '+';
SUB: '-';
WS : [ \t\r\n]+ -> skip;  // 跳过空格、制表符、换行符

然后使用如下的命令让 Antlr 编译规则文件,并生成相应的代码

shell 复制代码
antlr4 Hello.g4

Antlr4的规则文件

g4文件语法规则

前面我们创建了一个 Calculate.g4 文件,它是Antlr中的规则文件,现在我们来看看g4文件的规则。

grammar Calculate;

grammar 表示这是一个语法规则文件,Calculate是文件名,它必须和该规则文件的文件名相同。除了 grammar,还有 lexer,它表示词法分析的规则文件。它们的区别是,grammar Calculate 相对于 lexer Calculate 会多生成 XXXParserXXXListener 等语法解析相关文件。

通配符

markdown 复制代码
- `|`表示或
- `*`表示出现0次或以上
- `?`表示出现0次或者1次
- `+`表示出现1次或以上
- `~`表示取反,例如`~'"'`表示除`"`之外的字符

词法符号

词法符号:是一门语言的基本词汇符号,例如float,代表单精度浮点型。在Antlr中词法使用大写字母开头 ,如下面的示例的INT就是代表整数的词法符号。

vbnet 复制代码
//lexer文件

INT : [0-9]+ ;//匹配1个或多个数字

//匹配null
NULL_LITERAL:       'null';

//匹配true和false,| 是或的意思
BOOL_LITERAL:       'true'
            |       'false'
            ;

WS: [\t\r\n]+ -> skip //-> skip 表示跳过,这里是跳过空白字符

在g4文件中,关键字:内容;表示定义一个规则。其中单引号''引用的内容,表示完全匹配该内容。

语法

语法:是定义语言中的语义规则。例如void f()代表方法的语法。在Antlr中语法规则使用小写字母 ,如下面示例的 init 就是语法

csharp 复制代码
//grammar文件,以小写开头
//匹配 { value, value,value,...}
init: '{' value (',' value)* '}' ;

//表示一个value可以是嵌套的花括号结构,也可以是一个整数
value: init
     | INT
     ;

注意:在g4文件中,{}[]()在使用上是一个意思,代表子表达式。

标签语法

ini 复制代码
| expression bop=('*'|'/'|'%') expression  // 乘法、除法、取模
| expression bop=('+'|'-') expression      // 加法、减法

这里的bop=是一种标签(label)语法,用于为语法规则中的某个元素指定一个名称。

Calculate.g4

了解了 g4 规则文件的语法规则后,现在来看看 Calculate.g4 的内容就简单多了,代码如下所示:

g4 复制代码
// 表示这是一个语法规则文件
grammar Calculate;

// 设置生成的 java 文件所在的包名为 example.calculate
@header {
package example.calculate;
}

prog: expr EOF;

// # MulDiv 不是注释,而是给该语句设置标签
expr:  expr ('*'|'/') expr   # MulDiv
     | expr ('+'|'-') expr   # AddSub
     | INT                    # int
     | '(' expr ')'          # parens
     ;

INT:[0-9]+; // 匹配整数
MUL: '*'; // 匹配 *
DIV: '/'; // 匹配 /
ADD: '+'; // 匹配 +
SUB: '-'; // 匹配 -
WS : [ \t\r\n]+ -> skip;  // 跳过空格、制表符、换行符

如何使用 Antlr4 来实现四则运算

在 Antlr4 中执行 antlr4 -visitor Calculate.g4 就可以让 Antlr 能帮我们生成一个 Visitor 处理模式的框架。通过它我们可以很简单地执行自定义的命令。这里以Calculate.g4规则生成的代码为例,来实现四则运算的效果。

首先,我们需要创建一个 CustomCalculateVisitor 来继承通过 antlr4 -visitor Calculate.g4 命令生成的类。代码示例如下:

kotlin 复制代码
class CustomCalculateVisitor: CalculateBaseVisitor<Int>() {

    /**
     * 处理类似 1+2 的加法运算
     */
    override fun visitAddSub(ctx: CalculateParser.AddSubContext?): Int? {
        return when {
            ctx == null -> {
                0
            }
            ctx.ADD() != null -> {
                // visit 方法会根据 ctx.expr() 返回的类型执行其对应的操作。
                // 比如 ctx.expr(1) 返回的是 CalculateParser.MulDivContext,则最后会
                // 调用到 visitMulDiv 方法
                val left = visit(ctx.expr(0))
                val right = visit(ctx.expr(1))
                left + right
            }
            ctx.SUB() != null -> {
                val left = visit(ctx.expr(0))
                val right = visit(ctx.expr(1))
                left - right
            }
            else -> 0
        }
    }

    /**
     * 处理类似 1*2 的乘法运算
     */
    override fun visitMulDiv(ctx: CalculateParser.MulDivContext?): Int? {
        return when {
            ctx == null -> {
                0
            }
            ctx.MUL() != null -> {
                val left = visit(ctx.expr(0))
                val right = visit(ctx.expr(1))
                left * right
            }
            ctx.DIV() != null -> {
                val left = visit(ctx.expr(0))
                val right = visit(ctx.expr(1))
                if (right == 0) {
                    throw Exception("div zero")
                }
                left / right
            }
            else -> 0
        }
    }

    /**
     * 处理类似 1、2、3 这样的整数字面量
     */
    override fun visitInt(ctx: CalculateParser.IntContext?): Int? {
        return ctx?.INT()?.text?.toInt() ?: 0
    }

    /**
     * 处理类似 (1+2) 的括号运算
     */
    override fun visitParens(ctx: CalculateParser.ParensContext?): Int? {
        return when {
            ctx == null -> {
                0
            }
            // 假设括号为 (1+2), 那么 ctx.expr() 为 1+2
            else -> visit(ctx.expr())
        }
    }

}

然后我们就可以使用 CustomCalculateVisitor 来实现四则运算的功能。代码示例如下:

kotlin 复制代码
fun main() {
    val script = "1 + ( 2 + 3 ) * 4 + 6 / 3"
    //创建一个CharStream,从标准输入读取数据
    val input = CharStreams.fromString(script)
    // 新建一个词法分析器,处理输入的CharStreams
    val lexer = CalculateLexer(input)
    // 新建一个词法符号缓冲区,用于存储词法分析器将生成的词法符号
    val commonTokenStream = CommonTokenStream(lexer)
    // 新建一个语法分析器,处理词法符号缓冲区中的内容
    val parser = CalculateParser(commonTokenStream)
    val tree = parser.expr()
    val calculateVisitor = CustomCalculateVisitor()
    val result = calculateVisitor.visit(tree)
    println("result: $result")
}
相关推荐
醉雨清风2 天前
组件化场景下动态库与静态库依赖分析
编译原理
小墙程序员3 天前
编译原理教程(一)编译器的前端技术
编译原理
苏近之13 天前
如何为 Python 新增语法
python·源码阅读·编译原理
千千寰宇1 个月前
[语法分析/编译原理] Antlr : 开源语法分析工具
编译原理
JNU freshman2 个月前
编译原理实验 之 Tiny C语言编译程序实验 语法分析
编译原理
444A4E2 个月前
C++多态完全指南:从虚函数到底层虚表机制,一文彻底掌握
c++·编译原理
脏脏a2 个月前
程序环境和预处理
c语言·编译原理
l1n3x2 个月前
编译原理前端-词法分析
算法·编译原理
G皮T2 个月前
【Python Cookbook】字符串和文本(五):递归下降分析器
数据结构·python·正则表达式·字符串·编译原理·词法分析·语法解析