语法分析:LL(1)语法分析的实现及扩展的巴科斯范式

终于港到实现。。


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)的语法分析呢

递归程序一般可改写为栈+数据表的形式

下面是预测分析程序的构成

扫描二维码关注公众号,回复: 10972093 查看本文章
  • 控制程序。最早的压栈处理、根据现行栈顶符号和当前输入符号执行动作
  • 分析表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

发布了85 篇原创文章 · 获赞 83 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/kafmws/article/details/99341983