网课链接:https://www.bilibili.com/video/BV1NE411376V?p=20
文章目录
Introduction to Parsing
很多很重要的语句并不能用正则表达式或者有限自动机来表示,例如:
{
(
i
)
i
∣
i
≥
0
}
{ \{(^i)^i|i≥0\}}
{(i)i∣i≥0}。这指出了很多嵌套语句不能很好的匹配。cool
语言中if
语句有对应的fi
语句来标识这个语句的结束,但是很多其他语言中并没有结束标识,而是隐式地结束,这些不能用正则表达式匹配。
那什么正则表达式可以表达什么样的式子呢?为什么他们不足以识别任意的嵌套语句?
举例:
下面是一个识别偶数个1的有限自动机:
他只能识别奇偶,并不能识别对应的次数,因此他只能识别mod k
,k
是机器上的状态数,即不能完成嵌套语句的前后匹配。
解析器(Parser)
- 输入:来自词法分析程序(lexer)的令牌(token)序列。
- 输出:这个程序的解析树。
举例:
cool语言:if x = y then 1 else 2 fi
Parser input:IF ID = ID THEN INT ELSE INT FI
Parser output:
总结(lexer和parser的比较):
Phase | 输入 | 输出 |
---|---|---|
Lexer | 一连串字符(即字符串) | 一串令牌 |
Parser | 一串令牌 | 解析树 |
注意:
- 有时解析树是隐式的,所以编译器永远不会构建完整的解析树。
- 有些编译器将Lexer和Parser两个阶段合并成一个强大的Parser。但是大部分的编译器还是将两者分开的,因为正则表达式与词法分析非常匹配,所以Parsing单独处理。
上下文无关语法(Context-Free Grammars)
我们已经知道:
- 不是所有的令牌都是有效的程序。
- 解析器必须区分有效和无效的令牌。
所以我们需要:
- 一种语言来描述有效的令牌。
- 一种来区别有效或无效令牌的算法。
与上下文无关的语法(CFG)是一种来描述这种嵌套结构的自然的表示方式,它包括:
- 一组终端(T)
- 一组非终端(N)
- 一个起始符号(S, S∈N)
- 一组Production:
下面是关于 N N N和 T T T的特定的CFG的结果:
Productions
可以理解为规则,右边的项可以等价代换左边的项。 流程如下:
- 开始时是一个字符串,只有起始符号S。
- 将字符串中的所有非终端 X X X通过规则用Production的右边项替代( X − > Y 1 Y 2 . . . Y n X->Y_1Y_2...Y_n X−>Y1Y2...Yn)。
- 重复第2步直到没有非终端N。
a 1 − > a 2 − > . . . a n a_1->a_2->...a_n a1−>a2−>...an可以记作 a 0 − ∗ > a n a_0-^*>a_n a0−∗>an(在大于等于0步中,有操作可以使 a 0 a_0 a0到 a n a_n an)。
Terminals
设 G G G是上下文无关的语法,起始符号 S S S,那么语言 T ( G ) T(G) T(G)为: { a 1 . . . a n ∣ a i ∈ T ∧ S − ∗ > a 1 . . . a n } \{a_1...a_n|a_i∈T ∧S-^*>a_1...a_n \} {a1...an∣ai∈T∧S−∗>a1...an}。
- 终端是没有规则(Production)可以替换的。
- 一旦生成,终端是永久存在的。
- 终端应该是这种语言的令牌(token)。
举例:
cool语言:
if EXPR then EXPR else EXPR fi
可以看出非终端习惯上用大写字母表示,终端用小写字母表示。
id
可以导出EXPR->id
简单的算数表达式
举例:
此外,上下文无关的语法还需要:
- 我们目前定义的只给出了是或否的答案,还需要一个在输入中构建解析树的方法。
- 必须能很好地处理一些错误。
- 具体的CFG实现(比如bison)。
建立一个CFG十分重要:
- 很多语法产生相同的语言
- 很多工具都是语法敏感的
推导(Derivations)
一个Derivation是一系列的production:
S -> … -> … -> … -> … -> …
还可以画成树形结构:
- 起始符号作为树的根节点
- 对于一个Production: X − > Y 1 Y 2 . . . Y n X->Y_1Y_2...Y_n X−>Y1Y2...Yn,将 Y 1 Y 2 . . . Y n Y_1Y_2...Y_n Y1Y2...Yn加入作为 X X X的孩子节点。
举例:
语法:
E − > E + E ∣ E ∗ E ∣ ( E ) ∣ i d E -> E + E | E * E | (E) | id E−>E+E∣E∗E∣(E)∣id
字符串:
i d ∗ i d + i d id*id+id id∗id+id
推导及解析树(Parse Tree)如下:
这其实是一个left-most deriation
,每一步替换最左边的非终端。
对应的也有right-most derivation
:
可以画出树状图得出:最左和最右边的生成是具有相同的解析树的。一个生成树可能有很多Derivation,最左和最右生成是最重要的解析器实现。
解析树的特征:
- 在叶子节点有终端也有非终端
- 对于树的叶子节点内部进行有序(从左向右)遍历,可以得到原来输入的字符串(如上图即遍历id*id+id)
- 解析树显示了操作符的相关性,而输入字符串并没有
模糊的上下文语法(Ambiguity)
如果某一个字符串可以用多于一种的解析树表示,那么这个语法就是模糊的。或者说,对于某个字符串,有不止一个最左或最右生成。
例如,根据上面的语法,下面两个解析树都可以表示
i
d
∗
i
d
+
i
d
id*id+id
id∗id+id,则说明这种语法具有歧义。
处理上下文歧义(模糊)的方法
- 重写一种没有歧义的新语法。
例如上面的语法改成非模糊的可以写成:
E − > E ′ + E ∣ E E -> E' + E | E E−>E′+E∣E
E ′ − > i d ∗ E ′ ∣ i d ∣ ( E ) ∗ E ′ ∣ ( E ) E' -> id*E' | id | (E) * E' | (E) E′−>id∗E′∣id∣(E)∗E′∣(E)
这个语法可以理解为:E的作用就是可以变成无数多个E’相加,E’的作用是可以变成无数多个id或者(E)的相乘。
总而言之,这里所有的加法都要在乘法之前实现,这样在解析树中乘号的深度才比加号深。
根据这种语法,上面例子中的右边的解析树不复存在。
再举一个例子:
语法:
E − > i f E t h e n E E-> if\ E\ then\ E E−>if E then E
∣ i f E t h e n E e l s e E |\ if\ E\ then\ E\ else\ E ∣ if E then E else E
∣ O T H E R |\ OTHER ∣ OTHER
表达式:
i f E 1 t h e n i f E 2 t h e n E 3 e l s e E 4 if\ E_1\ then\ if\ E_2\ then\ E_3\ else\ E_4 if E1 then if E2 then E3 else E4
这个表达式可能就会存在两个解析树,第一种是 e l s e else else语句是外层 i f if if的分支,第二种是内层 i f if if的分支:
而我们想要的是 e l s e else else和最近的 i f if if匹配,我们可以修改语法:
E − > M I F / ∗ 所 有 的 t h e n 语 句 都 已 匹 配 ∗ / E\ ->\ MIF /* 所有的then语句都已匹配*/ E −> MIF/∗所有的then语句都已匹配∗/
∣ U I F / ∗ 有 些 t h e n 语 句 没 有 匹 配 ∗ / \ \ \ \ \ \ \ \ \ \ \ \ |\ UIF\ /* 有些then语句没有匹配*/ ∣ UIF /∗有些then语句没有匹配∗/
M I F − > i f E t h e n M I F e l s e M I F ∣ O T H E R MIF\ ->\ if\ E\ then\ MIF\ else\ MIF\ |\ OTHER MIF −> if E then MIF else MIF ∣ OTHER
U I F − > i f E t h e n E UIF\ ->\ if\ E\ then\ E UIF −> if E then E
∣ i f E t h e n M I F e l s e U I F \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ |\ if\ E\ then\ MIF\ else\ UIF ∣ if E then MIF else UIF
这种语法就排除了上图右侧的解析树的情况。
参考资料:
编译原理龙书