Antlr4 语言描述语法

要实现一门编程语言,首先需要构建一个程序,对输入的语句进行正确的处理。而语言实际是一系列有意义的语句组成的,语句又由词组组成,词组则是由更小的子词组和词汇符号组成。一般来说,一个程序能够分析执行语句,就称之为解释器,例如Python;如果一个程序能够将一门语言的语句转换为另一门语言的语句,就称之为翻译器。

而为了能够实现这样的功能,解释器或翻译器就需要能识别出一门特定语言的所有的有意义的词句、词组和子词组。例如a=100;,需要将其识别成为一个赋值语句,这就需要知道a是被赋值的对象,而100则是被赋予的值。

识别语言的程序成为语法分析器(Parser)或者句法分析器(Syntax Analyzer)。句法是只约束语言中的各个组成部分之间关系的规则;语法则是一系列规则的集合,每条规则表述出一种词句结构。Antlr4实现了一个描述其它语言的语法,可以根据该语法生成对应的语法分析器。

Antlr4 语法说明

Antlr4的语法文件可以描述一个语言,其尾缀为g4,一般而言,使用grammar关键字指定语法名,如下:

grammar Expr;

使用小写字母表示语法,语法可以使用|表示分支,使用;表示语法结束,并且语法可以进行递归嵌套,以表达式的语法为例,如下:

expr: expr ('*'|'/') expr
	| expr ('+'|'-') expr
	| INT
	| '(' expr ')'
	;

上面定义的表达式语法是一个最为经典的定义,注意越前的越先匹配,因此顶级表达式最先匹配的实际是一个表达式乘或除表达式,而后才是加减,再后则是数字;最后是括号运算。

括号运算在这里放到最后并非其优先级不高,而是一旦出现(就会进入该匹配规则,因此它写在什么顺序其实并不重要,如果希望符合人类直觉,写在第一行自然也是可以的。

使用大写字母表示词法,例如上面的INT实际就是一个词法,它的实际定义如下:

INT :   [0-9]+ ;

这表明它是由一系列数字组成的。

总计一下,Antlr4的语法规则如下:

  • 语法分析器的规则以小写字母开头
  • 词法分析器的规则以大写字母开头
  • 使用|分隔同一语言规则的若干备选分支,使用圆括号把一些符号组合成子规则
  • 语法可以递归解析,但是需要确保拥有递归出口

Antlr4 语法分析树

相关概念

以上面的赋值为例,语法文件如下:

stat: ID '=' expr ';';

Antlr4实际会根据该规则,产生一个自上而下的解析,并且形成一个语法分析树,判断一个语句是否合法,就看它与语法定义是否符合。

Antlr4在解析时尽量使用共享数据结构来节约内存,如下图所示: 存储的是字符串的位置而非字符串拷贝。

上图也展示了ParseTree的子类RuleNodeTerminalNode,两者分别是子树的叶子节点;但是RuleNode并非确定不变的,为了更好地支持对特定节点的元素访问,Antlr4会为每条规则都生成一个RuleNode子类;因此在上面的树中,子树的根节点实际是StatContextAssignContextExprContext 根节点包含了使用规则识别词组过程中的全部信息,被称之为上下文对象。每个上下文都知道自己识别出的词组中,开始和结束位置处的词法符号,同时提供访问该词组全部元素的途径。例如,AssignContext提供了方法ID()和方法expr()来访问标识符节点和表达式的子树。

给定这些类型的具体实现,就可以在深度优先遍历该语法树时进行一切所需的操作,例如计算结果,更新数据结构或者输出一类的事情;而实际上Antlr4会自动生成并遍历树,因此我们无需去关注遍历树的代码。

遍历机制

Antlr4为树的遍历提供了两种机制,分别为监听器与访问器;监听器实际就是回调函数,正如事件一般,当Antlr4访问到指定的节点时调用其回调函数;而访问器则是显式地控制遍历语法分析书的过程,通过显示的方法调用来访问对应的子节点。

监听器

为了将遍历树时触发的时间转化为监听器的调用,Antlr4提供了ParseTreeWalker类,在该类中,语法的每条规则都具有自己对应的enter方法和exit方法。例如,当遍历器访问到assgin规则对应的节点时,就会调用enterAssign方法,并且将AssignContext作为参数传递给它,在访问完assign节点的全部子节点后,就会调用exitAssign方法,图例如下: 而实际代码的调用顺序如下图:

访问器

有些时候,希望控制遍历语法树的过程,通过显式的方法调用来访问对应的子节点,则需要使用访问器模式了。访问器模式需要在命令行中加入-visitor来生成对应的访问器接口;同时如果需要禁用监听器,则需要再加入-no-listener

访问器模式下,语法中的每条规则对应接口中的一个visit方法,其对应操作过程如下图: