终于港到实现。。
LL(1)文法与LL(1)分析
当一个文法满足以下条件时,称这个语法为LL(1)语法,LL(1)语法可以通过LL(1)的分析方法进行分析
- 文法不含有左递归
- 同一非终结符的FIRST集合两两不相交
- 非终结符的FIRST集若包含 ε,则不能与FOLLOW集相交
LL(1)分析:
- 若当前待匹配符号属于当前非终结符的某个产生式的候选首符集中,则将该非终结符按此产生式推导
- 若当前待匹配符号 a 不属于当前非终结符 A 的任何一个候选首符集,
- 若存在产生式 A → ε 且 a 属于FOLLOW(A),则将 A 推导为 ε;
- 否则 a 在此处为语法错误
LL(1)语法分析是一种自上而下分析的方法,从文法开始符号出发匹配输入符号,语法树自上而下生成
第一个L指从左向右扫描输入串,第二个L指最左推导,(1)指每次向前看1个输入符号进行语法分析
LL(1)语法分析的实现
递归下降分析程序
顾名思义,采用递归的方式向下构建语法树
每个非终结符的推导都作为一个函数过程
例,有LL(1)文法G(E)如下,
E → T EE
EE → + T EE | ε
T → F TT
TT → * F TT | ε
F → (E) | i
void parser(){
token = lexer();//词法分析获取下一token
E();
}
void E(){//用E匹配当前token E → T EE
T();
EE();
}
void T(){//用T匹配当前token T → F TT
F();
TT();
}
void EE(){//EE → + T EE | ε
if(token == '+'){
token = lexer();//当前token ‘+’ 得到匹配,继续分析
T();
EE();
}
//无操作即可匹配 ε
//也可以显式匹配 ε
//else if(FOLLOW(EE).contains(token))return;
//else printfError();
//或者else if(!FOLLOW(EE).contains(token)){ printfError(); }
}
void F(){//F → (E) | i
if(token == '('){
token = lexer();
E();
if(token == ')'){ token = lexer(); }
else printfError();
}
else if(token == 'i'){ token = lexer(); }
else printError();
}
void TT(){//TT → * F TT | ε
if(token == '*'){
token = lexer();
F();
TT();
}
//无操作即将TT推导为 ε,是否可以推导为 ε 交给在后面的语法分析中验证
}
递归下降由此得名
预测分析程序
有一个问题,如果我们用于编写parser的语言不支持递归,该如何实现LL(1)的语法分析呢
递归程序一般可改写为栈+数据表的形式
下面是预测分析程序的构成
- 控制程序。最早的压栈处理、根据现行栈顶符号和当前输入符号执行动作
- 分析表M[A,a]矩阵,A ∈ VN,a ∈ VT 或 a == #(假设#是输入串结束标志),当要用非终结符 A,
匹配输入符号 a 时,查表M[A,a],根据M[A,a]的情况,进行响应的操作 - 分析栈,存放当前推导过程中的文法符号,用于当前匹配的符号均在栈顶
设栈顶元素为X,当前输入符号为a,分析过程如下:
- 假设输入串结束标志为 #,开始时在栈中压入 # 与文法开始符号 S
- 若 X == a == #,则分析结束且成功
- 若 X == a ≠ #,则 X 与 a 匹配,X 出栈,将下一输入符号保存至 a
- 若 X 是一个非终结符,则查表M[A,a]
- 若M[A,a]为一个产生式,则将 X 出栈,将产生式的右部反序入栈(若产生式右部为 ε 则不用入栈)
- 若M[A,a]中为出错标志,则调用出错诊察程序
与词法分析的表驱动法类似,预测分析程序也是使用 查表 + 流程控制 实现
LL(1)文法保证了语法分析的每一步都是确定的,因此我们就可以“预测”文法符号的推导方向
这大概是预测分析程序的“预测分析”由来
void parser(){
int unfinished = 1;
Stack stack = new Stack();
stack.push("#");
stack.push("S");//文法开始符号S
while(unfinished){
a = lexer();//获得待匹配输入符号
if(X ∈ 终结符){
if(X == a){ stack.pop(); }
else printError();
}
else {
if(X == "#"){
if(a == "#"){ unfinished = 0; }
else printError();
}
else if(M[X,a] == {X → X1X2X3…Xn}){
if(X1 == ε)continue;
stack.push(Xn);
......
stack.push(X3);
stack.push(X2);
stack.push(X1);
}else printError();
}
}
}
那么这个分析表是什么亚子呢?
预测分析表的构造
以文法G(E)为例
E → TE/
E/ → + TE/ | ε
T → FT/
T/ → * FT/ | ε
F → (E) | i
i | + | * | ( | ) | # | |
---|---|---|---|---|---|---|
E | E → TE/ | E → TE/ | ||||
E/ | E/ → + TE/ | E/ → ε | E/ → ε | |||
T | T → FT/ | T → FT/ | ||||
T/ | T/ → ε | T/ → * FT/ | T/ → ε | T/ → ε | ||
F | F → i | F → (E) |
相信很容易就可以发现规律,如果输入符号 a 包含在非终结符 A 的某个FIRST集中,
那么M[A,a] = 该FIRST集对应的产生式
如果 A 没有任何一个FIRST集 包含 a,
- 如果存在产生式 A → ε,且 a ∈ FOLLOW(A),则M[A,a] = A → ε
- 否则,a 不能够被分析表接受,出现语法错误,M[A,a] = 出错标志
预测分析程序的匹配过程
以输入串 i1 * i2 + i3 为例,预测分析程序的匹配过程如下
扩展的巴科斯范式
在元符号 → 或 ::= 和 | 的基础上,扩充几个元语言符号:
- 用花括号{α}表示闭包运算α*
- 用 {α}n0 表示可以任意重复[0,n]次
- 用方括号 [α] 表示{α}10,即 α | ε,α 可有可无,或者说是可选的
扩充的巴克斯范式,语义更强,便于表示左递归的消去和因子的提取
如我们一直用来举例的文法
E → T | E + T
T → F| T*F
F → i | (E)
用扩展的巴科斯范式可以表示成
E → T { +T }
T → F { *F }
F → i | (E)
然后有什么用呢?
可以画出下面的语法图奥
很清楚了吧,懂我的意思吧
void parser(){
token = lexer();//词法分析得到下一单词符号
E();
}
void E(){
T();
while(token == "+"){
token = lexer();
T();
}
}
void T(){
F();
while(token == "*"){
token = lexer();
F();
}
}
void F(){
if(token == "i"){
token = lexer();
}
else if(token == "("){
token = lexer();
E();
if(token == ")"){ token = lexer(); }
else printError();
}
else printError();
}
扩展的巴科斯范式引入了循环,使得语法描述中的重复可以直观地使用 循环 来描述,进而使用循环实现,而不一定是递归
显然,扩展的巴克斯范式给出的递归下降分析程序更加直观,没有更多引入的非终结符
斯巴拉西
LL(1)文法与二义性
奈斯,我们现在晓得如何构造分析表、实现预测分析程序了
然而某些语法,消除左递归、提取左公共因子后仍然不满足LL(1)文法的条件,因为它有二义性
比如描述选择结构的文法
S → if Condition S | if Condition S else S | statement
Condition → bool
提取左公因式后
S → if Condition SS/ | statement
S/ → else S | ε
Condition → bool
斯巴拉西
看哈这个文法的FIRST集和FOLLOW集
FIRST(S) → { { if },{ statement } } FOLLOW(S) → { #,else }
FIRST(S/) → { else,ε } FOLLOW(S/) → { #,else }
FIRST(Condition) → { bool } FOLLOW(Condition) → { if,statement }
明显可以看到,FIRST(S/)∩FOLLOW(S/) = { else },不符合LL(1)文法的条件
因为在用非终结符 S/ 匹配输入符号 else 时,可以选择把 S/ 推导为 else S,进一步即使 else 得到匹配,
也可以选择把 S/ 推导为 ε,让后面的符号来匹配这个 else,
也就是说,在非终结符 S/ 匹配输入符号 else 时,对应着两种不同的语法树,会产生歧义
这个二义性文法的分析表如下,可以看到分析表中有多重定义入口(一个格子里有多个产生式)
if | bool | else | statement | # | |
---|---|---|---|---|---|
S | S → if Condition SS/ | S → statement | |||
S/ | S/ → else S S/ → ε | S/ → ε | |||
Condition | Condition → bool |
上面这个问题是编程语言普遍存在的问题,它是 else 与 if 的结合次序的问题
if()
if(){}
else{}
那么下面的 else 是与第一个 if 匹配,还是与第二个 if 匹配呢
即在上图情况中,token⑥是由内层S/匹配与 if③ 结合,还是将内层S/推导为 ε 使⑥与外层的 if① 结合?
也就是说,
如果使用产生式 S/ → else S,那么 else 将与最近的未匹配 if 匹配
如果使用产生式 S/ → ε,那么 else 将与最远的未匹配 if 匹配
上面的例子表明,存在多重入口的分析表会产生不同的语法树,对应着不同的语义(二义性)
如果语言规范指明了,else 会与最近/远的 if 匹配,那么就可以通过人为方式解决冲突(删除不符合语义的产生式)
实际上大部分程序设计语言都是这么做的
一般的,程序设计语言都选择就近匹配,即只保留产生式 S/ → else S
if | bool | else | statement | # | |
---|---|---|---|---|---|
S | S → if Condition SS/ | S → statement | |||
S/ | S/ → else S | S/ → ε | |||
Condition | Condition → bool |
如下图
onemore thing,
G为LL(1)文法 ⇔ G的预测分析表不含有多重定义入口
且LL(1)文法不是二义的
证明略
所以说,即使语言是二义的,文法是二义的,构造出来的分析表也有冲突,也可以根据合理的语言规范消除冲突
2019/8/13