最近学了这玩意 顺便写了一个ll1的编译器 就具体说一下到底怎么编写的
通俗的说一个例子
1.A=>Ab
2.A=>a
这是两个规则,其中前面的大写A表示非终结符,可以推导出右边的式子。也就是如果有一个A,你可以将其变为Ab又或者为ab。
假设上述两个是规则的情况下如果出现一个字符串ab 那么我们怎么用自顶向下的方法分析呢?
这里我们需要用到pda 简单来说就是用到栈
首先我们将A推入栈中
为什么要将A放入?因为A是开始的符号。所有的句子都需要从开始符号开始推导。这是我们人为定义的。
此时的栈中就只有A (A)
因为A是非终结符,所谓非终结符就是规则的左边,因为可以继续推导,所以叫做非终结,所谓终结符就是不能推导,例如a,你无法再左侧找到他。A是非终结符,而所有的句子都是终结符组成,所以你不能进行匹配。无法匹配的情况下则自然需要进行继续推导,我们假设现在有一个表。这个表如何做成我们稍后再说。
然后我们将A取出 选择规则1 A=>Ab
然后将Ab推入栈中 变成(Ab)
然后我们继续把A取出 此时栈中只剩(b)
然后我们用第二条规则 将A转换成a 然后放回栈中
此时栈中的情况就是(ab) 很明显 a是终结符, 既然是终结符那么就可以进行匹配。所以我们将a取出,然后匹配句子中的a, 此时句子中就是(b),而栈中也只剩下了(b) 。句子的存放可以放到队列里面。因为句子是只出不进的,而推导符号是可以进出的,并且需要将推导的结果放到最前面,所以推导只能用栈。
所以我们需要创建一个stack专门存放推导过程,一个queue存放输入的句子。
那么将刚才的过程简单的说一遍, (A,ab,),这括号第一个参数为推导的栈,第二个参数为传入进来的句子,第三个记录使用的规则序号。
(A,ab,)
(Ab,ab,1)
(ab,ab,12)
(b,b,12)
(,,12)
这便是解析的过程,那么现在的问题是什么,大家也能看出来了,为什么?为什么要使用1规则而非2规则,这背后便是表的建立。
首先我们需要进行一系列的规则的改变,
A=>Aa
A=>a
B=>C
C=>Ba
上述4个规则我们发现A=>Aa可能出现这么一种情况,A=>Aa,然后Aa=>AAa,然后不断的重复,且这个是first follow集合无法解决的问题,什么是first集? 例如A=>a A=>b这两个规则,此时进入了一个a,明眼人会选择第一个规则,为什么?这便是所谓的first集合的作用,判断现在的符号是应该选择哪个规则,也就是规则右边的第一个终结符
1.S=>A
2.A=>a
3.A=>b
S的first集合是A的first集合,规则2的first集合是a,规则3的first集合是b,而规则2和规则3都属于A,而规则1的first集合便是A,所以S的first集合便是a和b。那么当有a或者b的时候,我们就可以利用规则1转化为A然后再用23的规则转化为a或者b。
那么什么事follow集合呢?follow集合的意思便是规则后面出现的第一个终结符,这又是什么意思呢?
1.S=>Ac
2.A=>空
3.A=>b
此时你的栈上出现了符号Ab,下一个字符是b,请问一下,此时你应该选择规则2还是3?你会说规则3,为什么?因为规则2的后面跟的是什么?是c,为什么是c呢,为什么不是b呢,因为follow集合,即规则之后可能出现的值。这就能解决大部分情况的问题,即只看第一个字符就能决定此时运用哪些规则。
例如我们现在有一个c,开始符号A,规则为下
A=>ab
A=>空
A=>Bc
B=>空
那么根据first集合我们知道规则1是不适合的,那么规则2和规则3选哪一个呢?我们根据follow集合知道如果选择规则2,那么后面的follow集合就是什么都没有,而规则3是否可行呢?答案是可行,为什么?我们利用first集合和follow集合建立一个lookahead集合。也就是向前看一个字符,规则4的first集合为空,但是他的follow集合是c,也就是说first(Bc)的集合便是c,因为B为空,所以规则3的first集便是c,而既然first集是c,那么就可以用规则3进行推导。
但是左递归的出现便带来了问题,
A=>Aa
A=>a
第一个规则的first集合为A,而A的first集合为a,第一个和第二个的first集合完全相同。请问你怎么选?无法进行选择。那么此时你就只能进行尝试,但是问题又来了。我尝试Aa,之后又遇到了相同的问题,第一个字符还是a,那么继续进行尝试,此时得到了AAa,就这样会陷入无限循环。
那么我们怎么消除这种直接的左递归呢?
我们仔细想一想,Aa的意思是什么?Aa的意思无非是要么变成Aaa,要么是变成aa。那么我们如果仔细想一下,首先A最后一定是会变成终结符a,所以我们直接合并两个规则,因为第一个符号一定是a,
A=>a?
后面的应该是什么呢?其实不难发现,后面要么为空,也就是第二个规则,要么是无数个a,也便是第一个规则。但是我们可以想的更透彻一点,一个直接左递归的规则不论怎么样,最后一定是可以不断的生成他后面的式子的,例如A=>Aabc,他每使用这个规则,实际上就是整个式子从后面增加了一个abc。
那么这个?就是1
1是什么?1是一个规则,1是我们增加的第一个规则,我们后面修改规则还需要不断的增加,所以最好的方法就是用int值代表规则,每增加一个规则,int值递增,这样就不怕出现规则名字太长的问题。
而无数个a可以怎么表示呢?很简单,
1=>a1
空怎么表示呢?
1=>空
那么消除左递归后结果就是,
A=>a1
1=>a1
1=>空
这样直接左递归就消除了,那么间接左递归怎么办呢?什么是间接左递归
S=>B
B=>S
B=>a
我们使用规则1 得到 B ,然后用规则2,得到S,然后反复循环形成了间接左递归,而间接左递归的形成是为什么?很简单,因为环形,S=>B=>S 那么我们如何把环形切断呢? 我们可以让已经出现过的规则不允许再次出现在第一个字符。
S=>B (S=true,B=>true)那么说明S和B都已经被访问过了,你不允许在第一个字符出现这个规则,因为就有可能形成环形。
那么看到第二个规则的时候我们发现了问题
B=>S (S=true,B=true) 哎?怎么规则出现过了啊?那现在怎么办呢?
我们将S换成他左边的式子
B=>B 接下来就变成了直接左递归,那么B=>a
最终获得的便是
S=>B
B=>a
这便将直接左递归消除了。
但是这个规则还是有可能有问题,例如什么?
A=>ab
A=>ac
此时进入一个字符a,请问你应该选择第一个规则还是第二个规则?
所以我们还需要继续改进这些规则。怎么做?我们将其合并,第一个字符既然都是a,那么我们就把他的第一个字符进行合并,然后后面的字符换成新的规则。
A=>a1
1=>b
1=>c
这样就能准确找到规则了。以上的步骤需要重复多次。
然后就是first集合和follow集合了,上面已经说过了,first集合即一个规则中出现的第一个终结符,如果规则第一个是非终结符,那么还要查看他是否为空。
A=>BC
B=>空
B=>d
C=>a
上面的first集合便是 A{d,空},B{d,空},C{a}
那么为什么要把空加进来?因为可以求follow集。
现在我们就可以求follow集合了。
B后面跟着C,所以C的first集合就属于B的follow集合,如果C可以为空,那么我们将followC的集合添加到followB身上。而对于followC来说,因为C处于规则1最后一项,所以A后面的符号也是C后面的符号,所以FollowA也是FollowC。
最终follow集合便是
A{END} B{a} C{END}
有了first集和follow集就能建立表了。首先我们还需要la集合,look ahead集合。通常我们是只看前面一个字符就足够了。
怎么做?
我们首先看第一个规则,A=>BC
根据first集合我们得知B的first集有d和空,所以当遇到d的时候,我们此时若栈中有A,就能利用此规则,然后此时我们还发现B有空,所以我们继续看,发现了C,C的first集合有a,所以当栈中出现a的时候我们也能使用第一个规则。
现在我们继续看第二条规则,B=>空, 那这怎么看呢?我们将followB的集合添加到这里来,也就是当栈中有B,此时出现a,我们就可以用规则2.
以此类推
最终的结果便是
la1{d,a}
la2{a}
la3{d}
la4{a}
有了la,我们就能进行分析了。
(a,A,)
(a,BC,1)查表得知可以使用规则1
(a,C,12)查表得知使用规则2
(a,a,124)查表使用规则4
(,,124)解析完成。