前言
文法规则的分析有两种方法, 一种是自顶向下,一种是自底向上。这里我们先来聊一聊自顶向下。自底向上可以看我另一篇自底向上分析总结
自顶向下包括两种方法:
- 递归(下降)子程序
- LL(1)
左递归
我们都知道我们要把文法规则写成递归下降子程序,要保证没有左递归和左公因子的问题(值得一提的是,尽管如此,但可能还是会有二义性的问题产生)。
看个例子吧:
A → A a ∣ b A → Aa | b A→Aa∣b
有两种方式,一种是用EBNF改写:
A → b { a } A → b \{a\} A→b{
a}
推导过程靠归纳得到。
另一种方式是改成右递归:
A → b A ′ A ′ → ε ∣ a A ′ A → bA' ~~~~~\\ A' → ε ~|~ aA' A→bA′ A′→ε ∣ aA′
在结束这一部分之前,给大家来一个进阶版的例子:
A → B a ∣ A a ∣ c B → B b ∣ A b ∣ d A → Ba ~|~ Aa ~|~ c \\ B → Bb ~|~ Ab ~|~ d A→Ba ∣ Aa ∣ cB→Bb ∣ Ab ∣ d
可以看到上面这个例子不止有直接左递归,还有间接左递归的问题。这个时候我们可以用这样的方法:
-
- 从上往下,消除直接左递归
-
- 看下一条文法规则,若存在上面文法的左边的非终结符号,就把上面得到的不含直接左递归的文法规则代入到这条规则中,再消去左递归即可。
接下来我们动手做一下,先把第一条规则转换一下, 比如这里采用右递归的方式改写:
A → B a A ′ ∣ c A ′ A ′ → a A ′ ∣ ε B → B b ∣ A b ∣ d A → BaA' ~|~ cA' \\ A' → aA' ~|~ ε ~~~~~~~~~\\ B → Bb ~|~ Ab ~|~ d ~~ A→BaA′ ∣ cA′A′→aA′ ∣ ε B→Bb ∣ Ab ∣ d
接下来把 B在左边的文法规则中的A用第一条文法规则代入
A → B a A ′ ∣ c A ′ A ′ → a A ′ ∣ ε B → B b ∣ B a A ′ b ∣ C A ′ b ∣ d A → BaA' ~|~ cA' ~~~~~~~~~~~~~~~~~\\ A' → aA' ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~\\ B → Bb ~|~ BaA'b ~|~ CA'b ~|~ d A→BaA′ ∣ cA′ A′→aA′ ∣ ε B→Bb ∣ BaA′b ∣ CA′b ∣ d
接下来再消去第三条文法规则中的直接左递归
A → B a A ′ ∣ c A ′ A ′ → a A ′ ∣ ε B → C A ′ b B ′ ∣ d B ′ B ′ → b B ′ ∣ a A ′ b B ′ ∣ ε A → BaA' | cA' ~~~~~~~~~\\ A' → aA' | ε ~~~~~~~~~~~~~~~~~\\ B → CA'bB' | dB' ~~~~~\\ B' → bB' ~|~ aA'bB' ~|~ ε A→BaA′∣cA′ A′→aA′∣ε B→CA′bB′∣dB′ B′→bB′ ∣ aA′bB′ ∣ ε
这样就消去了直接左递归和间接左递归了。
左公因子
这里给个简单的例子:
A → α β ∣ α γ A → αβ ~|~ αγ A→αβ ∣ αγ
改的方法很简单:
A → α A ′ A ′ → β ∣ γ A → αA' ~\\ A' → β ~|~ γ A→αA′ A′→β ∣ γ
依然我们这里来个升级版的例子:
S → b B ∣ A C c A → a A ∣ b B ∣ ε B → e ∣ d C → f ∣ ε S → bB ~|~ ACc ~~\\ A → aA ~|~ bB ~|~ ε \\ B → e ~|~ d ~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~ S→bB ∣ ACc A→aA ∣ bB ∣ εB→e ∣ d C→f ∣ ε
这里有间接左递归: S → ACc ,然后A → bB, 发现会与 S → bB产生二义性。其实我们消去左公因子和左递归的本质就是为了程序能够运行下去。照着含有左递归的文法直接写出来的递归下降子程序会产生栈溢出的问题,而按着左公因子的文法则会让程序具有二义性…
解决方法是:
将第二条文法规则代入第一条即可.
S → b B ∣ a A C c ∣ b B C c ∣ C c S → bB ~|~ aACc ~|~ bBCc ~|~ Cc S→bB ∣ aACc ∣ bBCc ∣ Cc
在提取左公因子
S → b B ( ε ∣ C c ) ∣ a A C c ∣ C c S → bB(ε ~|~ Cc) ~|~ aACc ~|~ Cc S→bB(ε ∣ Cc) ∣ aACc ∣ Cc
最终结果:
S → b B ( ε ∣ C c ) ∣ a A C c ∣ C c A → a A ∣ b B ∣ ε B → e ∣ d C → f ∣ ε S → bB(ε ~|~ Cc) ~|~ aACc ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~~~~~~~~~\\ B → e ~|~ d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ S→bB(ε ∣ Cc) ∣ aACc ∣ CcA→aA ∣ bB ∣ ε B→e ∣ d C→f ∣ ε
接下来我们用这个文法来写一下递归下降子程序。
递归下降子程序
写一个递归下降子程序实现下列文法规则
S → a A C c ∣ b B ( ε ∣ C c ) ∣ C c A → a A ∣ b B ∣ ε B → e ∣ d C → f ∣ ε S → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~~~~~~~~~\\ B → e ~|~ d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ S→aACc ∣ bB(ε ∣ Cc) ∣ CcA→aA ∣ bB ∣ ε B→e ∣ d C→f ∣ ε
方法: 遇到非终结符号就调用,遇到终结符号就匹配。
备注: 下列程序中token是全局变量。
先来看看match函数:
void match(char expectToken){
if (token == expectToken){
// 这里getToken指的是读取下一个字符,可以是用户输入,也可以是文件读入,也可以是一段字符串的下一个字符
// 因此这里我把这个读取下一个字符保存到token中抽象为下列这句代码了
token = getToken();
} else {
Error(); // Error函数再讲完这个例子后会仔细提一下
}
};
S → a A C c ∣ b B ( ε ∣ C c ) ∣ C c S → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc S→aACc ∣ bB(ε ∣ Cc) ∣ Cc
void S(){
if (token == 'a'){
match('a');
A();
C();
match('c');
} else if (token == 'b'){
match('b');
B();
// 这里涉及到求Cc的First集合待会儿会说 !!! First集合 !!!
if (token == 'f' || token == 'c') {
C(); // 实际上这里是可以优化的,为此会引入follow集合的概念 !!!Follow集合!!!
match('c');
} else if (token == 'f' || token == 'c') {
C(); // 实际上这里是可以优化的,为此会引入follow集合的概念
match('c');
} else {
Error();
}
}
A → a A ∣ b B ∣ ε A → aA ~|~ bB ~|~ ε \\ A→aA ∣ bB ∣ ε
void A(){
if (token == 'a'){
match('a');
A();
} else if (token == 'b'){
match('b');
B();
}
// 由于有 ε ,这里不能判断出错
}
B → e ∣ d B → e ~|~ d B→e ∣ d
void B(){
if (token == 'e'){
match('e');
} else if (token == 'd'){
match('d');
} else {
Error();
}
}
C → f ∣ ε C → f ~|~ ε C→f ∣ ε
void C(){
if (token == 'f'){
match('f');
}
// 由于有 ε ,这里不能判断出错
}
好了,递归子程序我们就写完了。
不过,我们还要讨论一个问题, 就是我们在写
S → a A C c ∣ b B ( ε ∣ C c ) ∣ C c S → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc S→aACc ∣ bB(ε ∣ Cc) ∣ Cc
void S(){
if (token == 'a'){
match('a');
A();
C();
match('c');
} else if (token == 'b'){
match('b');
B();
// 这里涉及到求Cc的First集合待会儿会说 !!! First集合 !!!
if (token == 'f' || token == 'c') {
C(); // 实际上这里是可以优化的,为此会引入follow集合的概念 !!!Follow集合!!!
match('c');
} else if (token == 'f' || token == 'c') {
C(); // 实际上这里是可以优化的,为此会引入follow集合的概念
match('c');
} else {
Error();
}
}
这个递归子程序的例子中,我们设计到两个概念,一个是First集合,一个是Follow集合。下面我们来讨论一下这个东西。
First 集合
给出一个上述例子的简化版文法
S → C c C → f ∣ ε S → Cc ~~\\ C → f ~|~ ε S→Cc C→f ∣ ε
我们会对第一条文法写出这样的程序
void S(){
if (token == 'f' || 'c'){
C();
match('c');
} else {
Error();
}
}
这里你可能会问,if (token == ’ f ’ || ‘c’) 判断条件怎么来的呢?
答案是求 Cc的First集合。
求First集合的算法如下:
First(x) = {
};
k = 1;
while (k <= n){
if (xk 为终结符号或 ε)
first(xk) = xk;
first(x) = first(x) ⋃ first(xk) - {
ε};
if (ε 不属于 first(xk))
break;
}
if (k == n + 1)
first(x) = first(x) ⋃ {
ε};
Follow集合
S → C c C → f ∣ ε S → Cc ~~\\ C → f ~|~ ε S→Cc C→f ∣ ε
其实这里的 S 程序可以写成这个样子:
void S(){
if (token == 'f'){
C();
match('c');
} else if(token == 'c'){
match('c');
} else {
Error();
}
}
为什么要这样写呢?我们看到C可以是指向 ε 的,那么也就是调用C这个函数的时候, 只是判断一下是不是 ’ f '而已, 但是程序却为此付出了大的代价: 操作系统要保存现在,将数据断点啥的压入运行栈,而保存一下又出栈了。多不划算啊。如果我们可以早点知道C什么时候不用进去就好了。此时我们可以引入Follow集合。
void C(){
if (token == 'f'){
match('f');
}
}
也就是说,如果我们知道Follow(C), 即C的下一个字符,那就可以判断用不用进去了。这就是Follow集合的含义。
我们看一下Follow集合的算法:
Follow(start-symbol) := {
$}
for all nonterminals A is not equal to start-symbol do Follow(A) := {
}
while there are changes to any Follow sets do
for each production A -> X1, X2, ..., Xn do
for each Xi that is a nonterminal do
add First(Xi+1, Xi+2...xi+n) - {
ε} to Follow(Xi)
if ε is in First(Xi+1Xi+2...Xn) then
add Follow(A) to Follow(Xi)
Error函数
这里我们提一下Error函数。
Error函数不应该只是简单地和用户说出错吧。
应该说一下是哪里错误了。
我们以上文提到的例子来改写一下
S → a A C c ∣ b B ( ε ∣ C c ) ∣ C c A → a A ∣ b B ∣ ε B → e ∣ d C → f ∣ ε S → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~~~~~~~~~\\ B → e ~|~ d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ S→aACc ∣ bB(ε ∣ Cc) ∣ CcA→aA ∣ bB ∣ ε B→e ∣ d C→f ∣ ε
void Error(int errorId, char expectToken){
char[4][200] = {
"S → aACc | bB(ε | Cc) | Cc", "A → aA | bB | ε",
"B → e | d", "C → f | ε"}
printf("error at %s, expected token is %c but not found!", char[errorId - 1], expectToken);
}
改写上面那些函数:
match函数:
void match(char expectToken, int errorId){
if (token == expectToken){
// 这里getToken指的是读取下一个字符,可以是用户输入,也可以是文件读入,也可以是一段字符串的下一个字符
// 因此这里我把这个读取下一个字符保存到token中抽象为下列这句代码了
token = getToken();
} else {
Error(errorId, expectToken); // Error函数再讲完这个例子后会仔细提一下
}
};
S → a A C c ∣ b B ( ε ∣ C c ) ∣ C c S → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc S→aACc ∣ bB(ε ∣ Cc) ∣ Cc
void S(){
if (token == 'a'){
match('a', 1);
A();
C();
match('c');
} else if (token == 'b'){
match('b', 1);
B();
// 这里涉及到求Cc的First集合待会儿会说 !!! First集合 !!!
if (token == 'f' || token == 'c') {
C(); // 实际上这里是可以优化的,为此会引入follow集合的概念 !!!Follow集合!!!
match('c', 1);
} else if (token == 'f' || token == 'c') {
C(); // 实际上这里是可以优化的,为此会引入follow集合的概念
match('c', 1);
} else {
Error(1, '-'); // 这里就不纠结这个细节了,给了'-', 大家可以尝试优化一下
}
}
A → a A ∣ b B ∣ ε A → aA ~|~ bB ~|~ ε \\ A→aA ∣ bB ∣ ε
void A(){
if (token == 'a'){
match('a', 2);
A();
} else if (token == 'b'){
match('b', 2);
B();
}
// 由于有 ε ,这里不能判断出错
}
B → e ∣ d B → e ~|~ d B→e ∣ d
void B(){
if (token == 'e'){
match('e', 3);
} else if (token == 'd'){
match('d', 3);
} else {
Error(3, '-'); // 这里就不纠结这个细节了,给了'-', 大家可以尝试优化一下
}
}
C → f ∣ ε C → f ~|~ ε C→f ∣ ε
void C(){
if (token == 'f'){
match('f', 4);
}
// 由于有 ε ,这里不能判断出错
}
LL(1) 分析方法
上面我们学会了如何书写一个递归下降子程序来实现一组文法规则,那么假如给一个文法规则,我们每次都要重新写文法规则识别程序,是比较繁琐的。有没有什么办法可以“一劳永逸”呢?
前辈们想出了用表达方法,以二维数据的方式来表达递归下降子程序的路径。
由于思想与递归下降子程序一样,因此写出LL(1)表格的时候也要先去掉左递归和左公因子的问题。
S → a A C c ∣ b B C c ∣ C c A → a A ∣ b B ∣ ε B → e ∣ d C → f ∣ ε S → aACc ~|~ bBCc ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~\\ B → e ~|~ d~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~ S→aACc ∣ bBCc ∣ CcA→aA ∣ bB ∣ ε B→e ∣ d C→f ∣ ε
a | b | c | d | e | f | |
---|---|---|---|---|---|---|
S | aACc | bBCc | Cc | Cc | ||
A | aA | bB | ||||
B | d | e | ||||
C | f |
这里还没完,还要计算Follow(A)和Follow(C)
- Follow(A) = {c, f}
- Follow(C) = {c}
然后将 ε 填入 Matrix[A][c], Matrix[A][f], Matrix[C][c]中。
a | b | c | d | e | f | |
---|---|---|---|---|---|---|
S | aACc | bBCc | Cc | Cc | ||
A | aA | bB | ε | ε | ||
B | d | e | ||||
C | ε | f |
然后再用个分析栈存储一下中间结果即可,为了提高性能,存储的顺序采取倒序形式。然后访问表格过程中,如果访问到空白的单元格,如Matrix[S][d] ,则产生出错。
思考
S → a A C c ∣ b B ( ε ∣ C c ) ∣ C c A → a A ∣ b B ∣ ε B → e ∣ d C → f ∣ ε S → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~~~~~~~~~\\ B → e ~|~ d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ S→aACc ∣ bB(ε ∣ Cc) ∣ CcA→aA ∣ bB ∣ ε B→e ∣ d C→f ∣ ε
用LL(1)中,Matrix[S][b]应该填什么?
(提示: 最长串匹配原则。
实战小例子
结语
这里我们回顾了一下自顶向下的知识点。自顶向下还是挺有趣的,特别是它还能解决运算符的优先级和左结合性,之前大概率都是用中缀转后缀的做法实现的。
好啦,有什么错误和建议欢迎大家留言哦。嘻嘻。
参考资料: 编译原理及实践 Kenneth.C.Louden 机械工业出版社 2000.3