ANTLR是什么
ANTLR是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。
如果要求开发人员自己编写一个编译期前端,那无疑对开发者的理论基础、技术功底的要求都非常高。但是,有了ANTLR,一切都变得易如反掌。开发人员需要做的只是定义一份描述该语言的语法文件,然后ANTLR会自动生成词法分析器和语法分析器,并把任意输入文本处理为可视化的语法树。你还可以访问语法树中的任意节点,实现自定义的业务逻辑。
它被广泛应用于学术领域和工业生产实践,是众多语言、工具和框架的基石。Twitter搜索使用ANTLR进行语法分析,每天处理超过20亿次查询;Hadoop生态系统中的Hive、Pig、数据仓库和分析系统所使用的语言都用到了ANTLR;Oracle公司在SQL开发者IDE和迁移工具中使用了ANTLR;NetBeans公司的IDE使用ANTLR来解析C++...
编译器(compiler):所有在计算机上运行的软件都是由某种程序设计语言编写的,常见的如C++语言、Java语言、SQL语言等。但是,在一个程序可以执行之前,它都需要被翻译成一种能够被计算机执行的形式(机器码)。完成这项翻译工作的软件系统被称为编译器(compiler)。
相关名词定义
语言的组成与分解
- 语言(language)由一系列有意义的语句组成
- 语句(sentence)由词组组成
- 词组(phrase)是由更小的子词组(subphrase)和词汇符号(vocabulary symbol)组成。
- 解释器(interpreter):一般来说,如果一个程序能够分析计算或者“执行”语句,我们就称之为解释器(interpreter)。这样的例子包括计算器、读取配置文件的程序和Python解释器。
- 翻译器(translator):如果一个程序能够将一门语言的语句转换为另外一门语言的语句,我们称之为翻译器(translator)。例如 Java到C#的转换器和普通的编译器。
词法分析器
- 词法符号(token):词法符号包含至少两部分信息:词法符号的类型(如INT、FLOAT等)和该词法符号对应的文本。
- 词法分析(lexical analysis):将字符聚集为词法符号(token)的过程称为词法分析或者词法符号化(tokenizing)。
- 词法分析器(lexer):我们把可以将输入文本转换为词法符号的程序称为词法分析器。词法分析器可以将相关的词法符号归类,例如INT(整数)、ID(标识符)、FLOAT(浮点数)等。
语法分析器
- 语法分析器(parser):识别语言的程序称为语法分析器或者句法分析器(syntax analyzer)。
- 句法(syntax)是指约束语言中的各个组成部分之间关系的规则。我们会通过ANTLR语法来指定语言的句法。
- 语法(grammar)是一系列规则的集合,每条规则表示一种词汇结构。
项目实践
目标
使用ANTLR识别包裹在花括号或者嵌套的花括号中的一些整数,像{1,2,3}和{1,{2,3},4}这样。这样的结构可以作为int数组或者C语言中的结构体的初始化语句
设计语法
首先需要定义目标语言的词法规则和语法规则。词法规则以大写字母开头,语法规则以小写字母开头。
词法规则定义
ANTLR支持正则表达式中的表达法。常见的词法结构如下
- 匹配字母
ID: ('a'..'z' | 'A'..'Z')+ ; //匹配1个或多个大小写字母
或者
ID: [a-zA-Z]+ ; //匹配1个或多个大小写字母
- 匹配数字:
INT:[0-9]+;
- 匹配浮点数:
FLOAT:DIGIT+'.'DIGIT*;//匹配1.123
fragment
DIGIT:[0-9] ;//匹配单个数字
fragment表示该规则不是词法符号,只是会被其他的词法规则使用。也就是说在语法规则中无法使用
- 匹配注释和空白字符
当词法分析器匹配到注释和空白字符的时候,我们通常希望将它们丢弃。否则文法规则就变成了一团乱麻,且十分容易出错。
WS: [ \t\n\r]+ -> skip; //匹配换行或空格
COMMENT_INPUT: '/' .? '/' -> skip; //匹配“/”或“*/”
语法规则定义
grammar ArrayInit;
//一条名为 init 的规则,它匹配一对花括号中的、逗号分隔的value
init : '{' value (','value)*'}';//必须匹配至少一个 value
//一个value 可以是嵌套的花括号结构,也可以是一个简单的整数,即INT词法符号
value : init
|INT
;
INT: [0-9]+ ; // 定义词法符号 INT,它由一个或多个数字组成
WS: [\t\r\n]+ -> skip; //定义词法规则“空白符号”,丢弃
生成语法分析树
第二个阶段是实际的语法分析过程:在这个过程中,输入的词法符号被“消费”以识别语句结构,在上例中即为赋值语句。
默认情况下,ANTLR生成的语法分析器会建造一种名为语法分析树(parse tree)或者句法树(syntax tree)的数据结构, 该数据结构记录了语法分析器识别出输入语句结构的过程,以及该结构的各组成部分。下图展示了数据在一个语言类应用程序中的基本流动过程。
- 根节点:根节点是最抽象的一个名字,在本例中即stat(statement的简写)。
- 内部节点:语法分析树的内部节点是词组名,这些名字用于识别它们的子节点,并将子节点归类。
- 叶子节点:语法分析树的叶子节点永远是输入的词法符号。
- 句子:也即符号的线性组合,本质上是语法分析树在人脑中的串行化
主程序实现
ANTLR的运行库提供了两种遍历语法分析树的机制,一种是通过监听器访问,一种是通过访问器访问。
通过监听器实现
生成语法分析树后,会对树进行遍历。ANTLR为每条规则都定义了监听器,并且提供了enterXXX和exitXXX的方法,分别对应进入某条规则和退出某条规则的动作,并且会将当前对应的语法分析树节点,也就是XXXContext的实例当作参数传递给它。
将上述词法文件和语法文件放入单独文件夹,然后运行antlr工具,会自动生成很多文件,如下图所示。
文件ArrayInitListener类给出了一些回调方法的定义,我们可以实现它来完成自定义的功能。 ArrayInitBaseListener是该接口的默认实现类,为其中的enter方法和exit方法提供了一个空实现。实现时只需要继承ArrayInitBaseListener类并覆盖需要的方法。下面是一个监听器的实现类。
//将类似{1,2,3}的short 数组初始化语句翻译为 "\u0001\u0002\u0003"
public class ShortToUnicodestring extends ArrayInitBaselistener {
//将{翻译为"
@override
public void enterInit (ArrayInitParser. Initcontext ctx) {
System.out.print('"');
}
//将}翻译为"
@override
public void exitInit (Array InitParser. Initcontext ctx) {
System.out.print('"');
}
//将每个整数翻译为四位的十六进制形式,然后加前缀\U
@override
public void enterValue (ArrayInitParser.Valuecontext ctx) {
//假定不存在嵌套结构
int value = Integer.valueOf(ctx.INT().getText());
System.out.printf(“\ug04x", value);
}
}
主程序
//导入 ANTLR 运行库
import org.antlr.v4.runtime.*;
import org.antlr.v4 .runtime .tree .*;
public class Translate {
public static void main(String[J args) throws Exception {
//新建一个 Charstream,从标淮输入读取数据
ANTLRInputStream input = new ANTLRInputStream (System.in);
// 新建一个词法分析器,处理输入的 Charstream
ArrayInitlexer lexer = new ArrayInitLexer (input);
//新建一个词法符号的缓冲区,用于存储词法分析器将生成的词法符号
CommonTokenstream tokens = new CommonTokenstream (lexer);
//新建一个语法分析器,处理词法符号缓冲区中的内容
ArrayInitParser parser = new ArrayInitparser(tokens);
ParseTree tree = parser.init();//针对 init规则,开始语法分析
//新建一个通用的、能够触发回调函数的语法分析树遍历器
ParseTreewalker walker = new ParseTreewalker();
//遍历语法分析过程中生成的语法分析树,触发回调
walker.walk (new ShortTounicodestring (), tree);
System.out.printin();// 翻译完成后,打印一个\n
}
}
通过访问器实现
有时候,我们希望控制遍历语法分析树的过程,通过显式的方法调用来访问子节点。下图是使用常见的访问者模式对我们的语法分析树进行操作的过程。
其中,粗虚线显示了对语法分析树进行深度优先遍历的过程。细虚线标示出访问器方法的调用顺序。我们可以在自己的程序代码中实现这个访问器接口,然后调用 visit()方法来开始对语法分析树的一次遍历。
ParseTree tree = . . . ; // tree 是语法分析得到的结果
MyVisitor v = new MyVisitor();
v.visit(tree);
ANTLR内部为访问者模式提供的支持代码会在根节点处调用visitStat()方法。接下来,visitStat()方法的实现将会调用visit()方法,并将所有子节点当作参数传递给它,从而继续遍历的过程。或者,visitMethod()方法可以显式调用 visitAssign()方法等。
在命令行中加入-visitor选项可以指示ANTLR为一个语法生成访问器接口 (visitor interface),语法中的每条规则对应接口中的一个visit方法。ANTLR会提供访问器接口和一个默认实现类,免去我们一切都要自行实现的麻烦。这样,我们就可以专注于那些我们感兴趣的方法,而无须覆盖接口中的方法
ANTLR语法和正则表达式的区别
熟悉正则表达式的读者可能会产生疑问,使用ANTLR来解决这么简单的识别问题是不是有点小题大做了?
实际上,由于嵌套的括号结构的存在,正则表达式无法识别这样的初始化语句。正则表达式没有存储的概念,它们无法记住之前匹配过的输入。因此,它们不能将左右括号正确配对,尤其是多层嵌套括号匹配
参考资料
《ANTLR 4 权威指南》Terence Parr著,机械工业出版社
《编译原理》第二版,Alfred V.Aho等著,机械工业出版社