grammarAnalysis
语法分析
通过 ANTLR4 生成语法分析器
此处我们研究一个名为 Cymbol 的语言,是 C 语言的子集(代码附后):
这样的文法有几个问题:
-
二义性:一段程序可以有多种解释方法。
1
2
3
4stat: 'if' expr 'then' stat
| 'if' expr 'then' stat 'else' stat
| expr
;假设此时有这样的程序:
if a then if b then c else d
,会出现二义性:if a then
if b then c else d
;if a then
if b then c
else d
。
这种语法歧义被称为“悬空的
else
”。如何修改这样有二义性的文法?
我们将文法修改为:
1
2
3
4
5
6
7
8
9stat: matched_stat | open stat ;
matched_stat: 'if' expr 'then' matched_stat 'else' matched_stat
| expr
;
open_stat: 'if' expr 'then' stat
| 'if' expr 'then' matched_stat 'else' open_stat
;改造后的文法实现了第一种解释。
为什么?
- 证明改造前后接受的语句集合是相同的;
- 证明改造后的文法无二义性。
证明留做习题(
但是注意到,通过 ANTLR4 测试改造前的文法,发现同样实现的是第一种解释,并无二义性。原因是 ANTLR4 会按照从上向下的顺序分配优先级,越往上的优先级越高,所以这种实现也是正确的。
-
运算符的结合性带来的二义性:
1
2
3
4expr: expr '*' expr
| expr '-' expr
| DIGIT
;对于此文法,
1-2-3
会被解释为(1-2)-3
或1-(2-3)
:在 ANTLR4 中,如果未明确运算符是左结合还是右结合,默认左结合,即上述表达式被解释为
(1-2)-3
。如果想指定右结合,可以在语句前加入
<assoc = right>
,如:1
2
3
4expr: '!' expr
| <assoc = right> expr '^' expr
| DIGIT
;考虑前缀运算符,这东西不带来歧义,只有一种解析方式,即右结合;后缀运算符只能左结合。
-
运算符的优先级带来的二义性:
1
2
3
4expr: expr '*' expr
| expr '-' expr
| DIGIT
;当一个运算符优先级更高时,在解析树中的深度会更深。在 ANTLR4 中这样写已经可以处理优先级的问题,因为写在前面的优先级更高。
如果不得已非要处理这种情况,可以通过两种方法:
-
改为左递归(左结合):
1
2
3
4
5
6
7
8
9expr: expr '-' term
| term
;
term: term '*' factor
| factor
;
factor: DIGIT;如此,对于一个语法单元
expr
,只会被左侧的expr
调用递归,这被称为左递归文法,这样限制了文法只能被左结合的方式进行解释。由于乘号优先级更高,所以要把乘号放在右侧,等待
expr
被解释完成后再进行解释。左递归文法:
e -> e - t -> e - t - t -> ...
-
改为右递归:
1
2
3
4
5
6
7expr: term expr_prime;
expr_prime: '-' term expr_prime;
term: factor term_prime;
term_prime: '*' factor term_prime;
factor: DIGIT;expr
表示的内容依然为term-term-term-...
,不同的是,通过引入新的语法单元prime
,expr
生成的方式为先生成一个term
,再不断生成-term
序列,这样就是右递归文法了。右递归文法:
e -> tEP -> t - tEP -> t - t - tEP -> ...
-
生成函数调用图
函数调用图表示的是函数之间的调用关系。
获取函数名有两个时机:
- 在进入
functionDecl
时,可以获得当前进入的函数名; - 在遇到
functionCall
时,即找到一个expr
表示的是ID '(' exprList? ')'
时,我们就可以得到被调用函数的函数名(即ID
)。
只要在图中将这两个函数名连边即可。