对输入的任意单词符号串,试图用一切可能的办法,从文法开始符号(树根)出发,自上而下、自左而右地建立起一棵语法分析树,使得该树的叶结点自左而右地排列起来,刚好就是所给的输入串。显然,这一过程应该与一个最左推导相对应。
方法特点:这实际上是一种穷举法,自上而下地试探,有时免不了要走回头路—回溯
存在的问题及解决方法
左递归问题
自顶向下分析为什么不能处理左递归文法?
如果在匹配输入串过程中,正好轮到要用非终结符 U 直接匹配输入串,那么就会用到 U 所对应的右部符号串 U… 去匹配;然后又轮到 U 去匹配,又要用到U的右部符号串U… 。于是形成无限循环。
解决方法
核心思想:避免出现左递归文法,将左递归文法转换为右递归。
而左递归的类型又分为直接左递归和间接左递归。
(1)直接左递归:直接存在于产生式中的左递归,如P→Pα,P∈VN,α∈(VT∪VN)*,这种直接左递归可以通过扫描产生式发现,也容易解决—改写产生式即可。
解决方法可以总结为:
一个很常用的例子便是算术表达式:
(2)间接递归:一眼看不出有左递归的文法
解决方法的思路:
回溯问题
会出现回溯问题,主要的原因便是因为父亲结点生成孩子结点时的盲目性。因此便有了First集和Follow集:
理论说完了,那就来一个例子:
首先对于First集:
因为E的推导为非终结符T,因此E的first集等价于T的first集,而T的推导为非终结符F,因此T的first集等价于F的first集,而F的推导式中第一个字符为(
和i
,因此E、T、F的first集便是{ ( , i }。
而E1的first集可以很容易就发现为{ + , ε },T1的first集为{ * , ε }。
而对于Follow集:
首先将#加入到E的Follow集中,而E处在推导式右端的情况只有(E),因此将)加入到E的Follow集,然后再将E的Follow集加入到E1的Follow集中。然后将E1的First集除ε之外的元素加入到T的Follow集,又因为E1可以为ε,因此需要将E的Follow集加入到T的Follow集中。后面以此类推。
递归下降分析
对文法的每一个非终结符都编写一个分析过程,当根据文法和当时的输入符号预测到要用某个非终结符去匹配输入串时,就调用该非终结符的分析过程。
由于高级程序设计语言的语法结构中存在不少递归定义,所以该方法通常又被称做递归子程序法。
递归子程序法的优点:
- 程序结构层次清晰,易于实现
递归子程序法的不足:
- 对文法有要求,适用范围受限
- 只有所使用的高级语言具有实现递归过程的编译系统时才有实际意义
- 程序运行速度慢,占用内存空间多,效率不高
非递归的预测分析LL(1)文法
回想一下前面介绍过的自上而下分析的问题和解决思路,关键是对于某个非终结符A和当前输入符号a,如何去选择A的候选。该方法就是针对这点而来,而且没有用到递归算法。
对于一个消除了左递归且候选首符集两两不相交的文法,可以根据其产生式把所有的对 偶(A,a)构造出来,每一个这样的对偶“指示”了面对“岔路”时应该如何选择,这些对偶构成一张“地图”(分析表)。
通常把分析表不含多重定义入口的文法称为LL(1)文法,第一个L代表从左到右扫描输入串,第二个L代表产生最左推导,1代表在决定语法分析器的每步动作时向前看1个输入符号即可。