antlr4-理论基础
Table of Contents
解析文件example #
grammar calc;
MUL: '*';
DIV: '/';
ADD: '+';
SUB: '-';
NUMBER: [0-9]+;
WHITESPACE: [ \r\n\t]+ -> skip;
start : expression EOF;
expression
: expression op=('*'|'/') expression # MulDiv
| expression op=('+'|'-') expression # AddSub
| NUMBER # Number
;
- 语法文件通常以grammar开头,并且文件名与定义的grammar相同(该例中文件名必须为calc.g4)。
- 语法规则必须以小写字母开头。
- 词法规则必须以大写字母开头。
- 使用|来分割一个规则的若干备用分支。
- 用#来为备选分支设置标签,只有被设置了标签的分支才会生成”事件方法“。
- 可以接收子规则(例子中的op)。
- 标签用来区分备选子规则。expression只会生成“进入”和“退出”的事件,因此对备选分支需要进一步细化。
语法模式 #
序列模式 #
序列即一列元素,如表示一列数字1,2,3,4,5
则可以表示为num : INT(,INT)*;
重复的元素可用圆括号包裹。
*
: 表示没有或者多个+
: 表示1个或多个?
: 表示没有或1个
选择模式 #
即备选分支,用|来分割多个备选分支,如
field : INT | STRING;
如果有多个备选分支同时符合,则选择最前边的备选分支。
词法符号依赖模式 #
一个词法符号依赖多个词法符号,如
expr: '(' field ')'
ANTLR核心标记 #
用法 | 描述 |
---|---|
x | 匹配词法符号、规则或者子规则x |
x..y | 匹配一列规则元素 |
(…|…|…) | 具有备选分支的子规则 |
x? | 匹配x或者忽略它 |
x* | 匹配x 0次或多次 |
x+ | 匹配x 1次或多次 |
r: …; | 定义规则r |
r: … | … |…; | 具有备选分支的规则 |
入门专用词法规则 #
匹配标识符 #
ANTLR支持正则表达式中用于表 示字符集的缩写:
ID: [a-zA-Z]+; // 匹配一个或多个大小写字符
使用序列模式+选择模式:
ID: ('a'..'z' | 'A'..'Z')+; // 匹配一个或多个大小写字符
匹配数字 #
整数:
INT: '0'..'9'+; // 匹配1个或多个数字
或者:
INT: [0-9]+; // 匹配1个或多个数字
浮点数:
FLOAT: DIGIT+ '.' DIGIT* // 匹配 1. 39. 3.14159等
| '.' DIGIT+ // 匹配 .14159
;
fragment
DIGIT : [0-9] // 匹配单个数字
将一条规则声明为fragment可以告诉ANTLR,该规则本身不是一个词法符号,它只会被其他 的词法规则使用。这意味着我们不能在语法规则中引用DIGIT。
匹配字符串常量 #
STRING: '"' .*? '"';
字符串是用双引号包裹的任意字符序列。
其中.*
表示任意字符,?
表示非贪婪匹配——若是贪婪匹配,则该表达式能够匹配任意内容。
上述规则不能匹配包含双引号的字符串,需要用转义字符\
。
STRING: '"' (ESC|.)*? '"';
fragment
ESC: '\\"' | '\\\\' // 双字符序列\" 和\\
ANTLR语法本身需要对转义字符\
进行转义,因此我们需要\\
来表示单个反斜 杠字符。
(ESC|.)*?
循环在看到后续子规则含有一个未转义的双引号时终止。
匹配注释和空白字符 #
当词法分析器匹配到注释和空白字符的时候,我们通常希望将 它们丢弃。这样,语法分析器就不必处理注释和空白字符了。
使用skip
指令通知词法分析器将它们丢弃.
丢弃注释:
LINE_COMMENT: '//' .*? '\n' -> skip ; // 匹配: '//' 任意字符序列 '\n'
COMMENT: '/*' .*? '*/' -> skip ; // 匹配: '/*' 任意字符序列 '*/'
丢弃空白字符:
WS: ( ' ' | '\t' | '\r' | '\n' )+ -> skip ;
或者:
WS: [ \t\r\n]+ -> skip;
通用经验 #
- 在词法分析器中匹配并丢弃任何语法分析器无须知晓的东西。
- 由词法分析器来匹配类似标识符、关键字、字符串和数字的常见词法符号。
- 将语法分析器无须区分的词法结构归为同一个词法符号类型。例如,如果我们的程序对待整数和浮点数的方式是一致的,那就把它们都归为NUMBER类型的词法符号。 没必要传给语法分析器不同的类型。
- 将任何语法分析器可以以相同方式处理的实体归为一类。例如,如果语法分析器不 关心XML标签的内容,词法分析器就可以将尖括号中的所有内容归为一个名为TAG的 词法符号类型。
- 如果语法分析器需要把一种类型的文本拆开处理,那么词法分析器就应 该将它的各组成部分作为独立的词法符号输送给语法分析器。
语法规则 #
右递归 #
如2^2^2
表示2^4
而非4^2
,即 需要先计算右边后计算左边。
这时需要指定后缀<assoc=right>
,即:
<assoc=right> expr '^' expr
词法和语法优先级机制 #
grammar test;
enumDef : 'enum' '{'...'}';
...
FOR : 'for';
...
ID : [a-zA-Z]+; //不会匹配enum和for
- ANTLR从文法规则中筛选出所有的字符串常量, 并将它们和词法规则放在一起。’enum’这样的字符串常量被隐式定义为词法规则, 然后放置在文法规则之后、显式定义的词法规则之前.
- ANTLR词法分析器解决歧义问题的方法是优先使用位置靠前的词法规则。这意味着,ID规则必须定义在所有的关键字规则之后,在上面的例子中,它在FOR规则之后。ANTLR将为字符串常量隐式生 成的词法规则放在显式定义的词法规则之前,所以它们总是拥有最高的优先级。因此,在本例中,’enum’被自动赋予了比ID更高的优先级.
访问语法树 #
解析语法和业务逻辑之间应该是解耦的。antlr提供了两种模式来访问语法树:访问器和监听器。
访问器机制和监听器机制的最大的区别在于,监听器的方法会被ANTLR提供的遍历器对象自动调用,而在访问器的方法中,必须显式调用visit方法来访问子节点。忘记调用visit()的后果就是对应的子树将不会被访问。
监听器能够对特定规则的进入和退出事件(即识别到某些词组的事件)作出响应,这些事件分别由语法分析树遍历器在开始和完成对节点的访问时触发。
这种基于监听器的方法十分巧妙,因为所有的遍历过程和方法触发都是自动进行 的。有些时候,自动进行的遍历反而成为一个缺陷,因为我们无法控制遍历的过 程。例如,我们可能希望遍历一个C语言程序的语法分析树,跳过对代表函数体的子树的访问,从而达到忽略函数内容的目的。此外,监听器的事件方法也无法利用方法的返回值来传递数据。当需要控制遍历过程,或者希望事件方法返回值时,我们可以使用访问者模式。
三种在事件方法间共享数据的方案 #
- 原生语言的调用栈:访问器返回一个用户指定类型的值。不过,如果访问器需要传递参数,那就必须使用下面两种方案。
- 基于栈的:在上下文类中维护一个栈字段,模拟参数和返回值的入栈和出栈。
- 标注:在上下文类中维护一个Map字段,用对应的值来标注节点。
错误自动恢复 #
通过扫描后续词法符号来恢复 #
语法分析器知道自己无法使用当前规 则匹配当前输入,它会持续丢弃后续词法符号,直至发现一个可以匹配本规则中断 位置之后的某条子规则的词法符号。
从不匹配的词法符号中恢复 #
在语法分析过程中,如果词法符号不符合预期,就会通知错误监听器并重新同步。为了完成同步,分析器可以在三种策略中选择一个执行:
- 移除一个词法符号
- 添加一个词法符号
- 抛出异常,跳过,继续解析
从子规则的错误中恢复 #
如果子规则是一个循环结构,即(...)*
或(...) +
,在遇到错误时,语法分析器会尝试进行积极的恢复,使得自己留在循环内部。在 成功地匹配到循环的某个备选分支之后,语法分析器会持续消费词法符号,直到发现满足下列条件之一的词法符号为止:
- 循环的另一次迭代
- 紧跟在循环之后的内容
- 当前规则的重新同步集合中的元素