文章目录
语法分析中的自顶向下分析方法
在编译原理中,语法分析是将词法分析得到的单词序列分析并构建成语法树的重要阶段。自顶向下分析是语法分析的一种重要方式,它从语法树的根节点开始,试图逐步向下推导出与输入单词序列匹配的叶子节点。接下来,我们就深入探讨自顶向下分析的各个方面。
一、确定的自顶向下分析思想
确定的自顶向下分析方法,是基于对输入符号的准确预测,从开始符号出发,按照一定的规则逐步推导,以构建语法树。其核心思想是在每一步推导中,根据当前的非终结符和输入符号,能够唯一地确定使用哪个产生式进行推导。
例如,对于一个简单的算术表达式语法:
E -> E + T | T
T -> T * F | F
F -> ( E ) | id
假设输入串为“id + id * id”,从开始符号 E 出发,看到第一个单词是“id”,根据上述产生式,由于“id”可以匹配“T”进而匹配“E”(通过“E -> T”这条产生式),所以可以确定使用“E -> T”进行推导。这种确定性的推导过程就是确定的自顶向下分析的关键特征。
在确定的自顶向下分析中,要求文法不能存在左递归(后文会详细介绍左递归及处理方法)和公共左因子(需要提取左公共因子以满足分析要求),否则会导致分析过程的不确定性。
二、LL(1)文法的判别
LL(1)文法是一类可以进行确定的自顶向下分析的文法。“LL(1)”中的第一个“L”表示从左到右扫描输入串,第二个“L”表示最左推导,“1”表示在每一步推导中只需要向前看一个输入符号就能确定使用哪个产生式。
判别一个文法是否为 LL(1)文法,需要计算文法的三个重要集合:
-
FIRST 集合:对于一个符号(终结符或非终结符)A,FIRST(A) 是指从 A 出发能够推导出的所有终结符的集合,如果 A 能推导出 ε(空串),则 ε 也属于 FIRST(A)。
例如,对于前面的算术表达式文法:- FIRST(E) = { (, id }
- FIRST(T) = { (, id }
- FIRST(F) = { (, id }
-
FOLLOW 集合:对于一个非终结符 A,FOLLOW(A) 是指在所有句型中,紧跟在 A 后面的终结符或 #(输入串结束标志)的集合。
例如,对于上述文法:- FOLLOW(E) = { ), # }
- FOLLOW(T) = { +, ), # }
- FOLLOW(F) = { *, +, ), # }
-
SELECT 集合:对于产生式 A -> α,如果 α 不能推导出 ε,则 SELECT(A -> α) = FIRST(α);如果 α 能推导出 ε,则 SELECT(A -> α) = FIRST(α) - { ε } ∪ FOLLOW(A)。
一个文法是 LL(1)文法的充分必要条件是:对于文法中的任意两个产生式 A -> α 和 A -> β,满足 SELECT(A -> α) ∩ SELECT(A -> β) = ∅。
三、某些非 LL(1)文法到 LL(1)文法的等价变换
实际中,很多文法可能不是 LL(1)文法,但可以通过一些等价变换使其成为 LL(1)文法,从而能够进行确定的自顶向下分析。
(一)提取左公共因子
当一个非终结符的多个产生式具有相同的左公共因子时,需要提取左公共因子,以消除分析时的不确定性。
例如,对于文法:
S -> aAb | aAc
A -> d | e
可以提取左公共因子“a”,将文法变换为:
S -> aA'
A' -> b | c
A -> d | e
这样变换后,在对 S 进行推导时,就不会因为看到“a”而无法确定使用哪个产生式了。
(二)消除左递归
左递归是指一个非终结符 A 存在推导 A -> Aα(直接左递归)或通过其他非终结符间接推导出 A -> Aα(间接左递归)的情况。左递归会导致确定的自顶向下分析无法进行,因为在对 A 进行推导时,会陷入无限循环。
对于直接左递归,例如文法:
E -> E + T | T
可以通过引入一个新的非终结符 E’,将其变换为:
E -> T E'
E' -> + T E' | ε
对于间接左递归,需要先通过替换等方法将其转化为直接左递归,再按照上述方法消除直接左递归。
四、不确定的自顶向下分析思想
与确定的自顶向下分析不同,不确定的自顶向下分析在推导过程中,对于当前的非终结符和输入符号,可能无法唯一确定使用哪个产生式,需要进行试探和回溯。
例如,对于文法:
S -> aS | b
当输入串为“ab”时,从 S 开始推导,看到第一个符号“a”,既可以使用“S -> aS”进行推导,也可能在后续发现应该使用“S -> b”。这种情况下就需要进行试探,如果试探失败就回溯到之前的状态重新选择产生式。由于不确定的自顶向下分析需要大量的试探和回溯,效率较低,所以在实际应用中通常会尽量将文法转换为 LL(1)文法,采用确定的自顶向下分析。
五、LL(1)分析的实现
LL(1)分析的实现主要有两种方式:递归下降 LL(1)分析程序和表驱动 LL(1)分析程序。
(一)递归下降 LL(1)分析程序
递归下降 LL(1)分析程序是根据文法的产生式,为每个非终结符编写一个对应的递归函数。在函数中,根据输入符号和当前非终结符,选择合适的产生式进行推导。
例如,对于前面的算术表达式文法:
def E():
T()
E_prime()
def E_prime():
if lookahead == '+':
match('+')
T()
E_prime()
elif lookahead in FOLLOW_E:
pass
else:
error()
def T():
F()
T_prime()
def T_prime():
if lookahead == '*':
match('*')
F()
T_prime()
elif lookahead in FOLLOW_T:
pass
else:
error()
def F():
if lookahead == '(':
match('(')
E()
match(')')
elif lookahead == 'id':
match('id')
else:
error()
def match(token):
global lookahead
if lookahead == token:
lookahead = get_next_token()
else:
error()
def error():
print("语法错误")
# 初始化输入串和当前符号
input_string = "id + id * id"
tokens = input_string.split()
lookahead = tokens.pop(0)
E()
上述代码展示了递归下降 LL(1)分析程序的基本结构,通过递归调用函数实现对输入串的语法分析。
(二)表驱动 LL(1)分析程序
表驱动 LL(1)分析程序使用一个分析表来驱动分析过程。分析表是一个二维表,行表示非终结符,列表示终结符和输入结束标志 #。表中的元素表示在当前非终结符和输入符号下应该使用的产生式。
在分析时,通过查询分析表来确定每一步的推导。例如,对于前面的算术表达式文法,构建的分析表如下(简化示意):
非终结符 | ( | ) | + | * | id | # |
---|---|---|---|---|---|---|
E | E -> T E’ | E -> T E’ | E -> T E’ | |||
E’ | E’ -> ε | E’ -> + T E’ | E’ -> ε | |||
T | T -> F T’ | T -> F T’ | T -> F T’ | |||
T’ | T’ -> ε | T’ -> * F T’ | T’ -> ε | |||
F | F -> ( E ) | F -> id |
分析时,从开始符号 E 开始,根据当前的非终结符和输入符号查询分析表,按照表中的指示进行推导,直到分析完整个输入串或发现错误。
六、LL(1)分析中的出错处理
在 LL(1)分析过程中,可能会遇到语法错误,需要进行相应的出错处理。常见的出错处理方法有应急恢复、短语层恢复以及特定语言(如 PL/0)语法分析程序的错误处理。
(一)应急恢复
应急恢复是一种简单的出错处理方法。当发现语法错误时,分析器跳过一些输入符号,直到遇到某个“同步符号”(通常选择在 FOLLOW 集合中的符号),然后从该点继续分析。
例如,在分析算术表达式时,如果遇到了一个不期望的符号,分析器可以跳过一些符号,直到遇到“+”“*”“(”“)”或输入结束标志“#”等同步符号,再继续进行分析。
(二)短语层恢复
短语层恢复是在发现错误时,尝试将输入串中错误附近的部分调整为一个合法的短语(语法单位),然后继续分析。
例如,当输入串为“id + + id”时,发现“+ +”是错误的,分析器可以将其调整为“id + id”(假设这是合理的调整),然后继续进行分析。这种方法需要对语法结构有更深入的理解和判断。
(三)PL/0语法分析程序的错误处理
PL/0 语法分析程序在处理错误时,结合了应急恢复和一些针对 PL/0 语言特点的处理方式。当检测到语法错误时,它会输出错误信息,指出错误的位置和类型,并尝试恢复分析,以便能够继续处理后续的输入,尽可能多地发现潜在的错误。
通过对自顶向下分析的全面了解,从确定与不确定的分析思想,到 LL(1)文法的判别和变换,再到分析的实现以及出错处理,我们对编译原理中的语法分析有了更深入的认识,这对于理解和实现编译器的语法分析模块具有重要的指导意义。