BNF和EBNF(extended BNF)
此两种文法描述语言大体相同,只是EBNF比BNF多了重复描述符{...}和可选描述符[...],而BNF中只能通过递归来表示重复。BNF与EBNF的具体定义这里不展开。只简单举个例子:
1、一个b后跟任意个a,如b, ba, baa, baaa
BNF表示:A
→
Aa|b
EBNF表示:A
→
b{a}
2、if语句(忽略空格)
BNF表示:
statement
→
if_statement | other
if_statement
→
if (exp) statement | if (exp) statement else statement
exp
→
0|1
EBNF表示:
statement
→
if_statement | other
if_statement
→
if (exp) statement[else statement]
exp
→
0|1
3、Tiny语言的文法描述(摘自教材)
BNF:
EBNF:
语义动作
分析到某个语法时所要执行的动作(代码)
如算式计算,遇到A
→
b+c时,计算b+c的值并反馈给A做下一步计算。
First集合与Follow集合
First(A)表示非终结符号A包含的第一个终结符号,如A
→
acB | b,则First(A)={a,b}
Follow(A)表示非终结符号A后面紧跟着的终结符号,如A
→
B[c],B
→
a
,
则:Follow(A)={$},Follow(B)={c,Follow(A)=$},$表示结束符
好了,入正题,文法分析的方法有哪些呢?
两种思路:
自顶向下分析
和
自底而上分析
而
自顶向下分析
又分为两种思路:
回溯法
和
预测分析
回溯法通用性较强,但性能较低,故一般不用于编译器,这里不做介绍
自顶向下是从非终结符到终结符方向分析,即文法规则从箭头左边到箭头右边
自底而上是从终结符到非终结符方向分析,即文法规则从箭头右边到箭头左边,
箭头右边的成分替换成左边的过程叫归约。
例:
T
→
aS
S
→
ab
自顶向下的思路是:
1、T
2、aS(T换成aS)
3、S(匹配a)
4、ab(S换成ab)
5、b(匹配a)
6、空(匹配b)
自底而上的思路是:
1、a(匹配a)
2、aa(匹配a)
3、aab(匹配b,有合适的文法规则可用于归约
S
→
ab
)
4、aS(归约,仍有合适的文法规则可用于归约
T
→
aS)
5、T(归约)
6、完成
具体算法:
1、自顶向下:递归下降分析法,LL(1)分析,LL(K)分析
注:第一个L(left)表示从左到右分析字符串,第二个L(left)表示最左推导,即文法规则中从左到右的推导(如上自顶向下的思路例子),K为一个整数,表示算法中需要向前(右)探测多少个字符才能决定使用哪一条文法规则
2、自底而上:LR(0)分析,LR(1)分析,SLR(1)分析,LALR(1)分析,LR(K)等
注:第一个L(left)表示从左到右分析字符串,第一个R表示从右到左的推导,K为整数,同上。其中SLR(1)和LALR(1)是LR(1)的不同改进方法。
上面提到的算法中只有递归下降分析法是用递归函数调用实现的,其余皆为栈的方式,固定算法,数据驱动,文法规则以数据表(table)的方式呈现,故不同的文法规则规则只是换了不同的数据设置。函数调用的方法代码易懂,贴合人的思维,故一般用于比较简单的手工编写,而栈的方式一般是用文法规则自动生成分析代码。
笔者认为,实际应用比较多的是递归下降分析法和LALR(1),所以这里只介绍这两种分析方法。
(原因:
自顶向下发分析方法中文法不能有左递归(A→Ab)和左公因子(A→ab|B, B→abc),左递归会造成无穷展开,左公因子会造成先行预测的位数难以确定。故自顶向下分析发对文法描述有一定的限制,一般不用于自动生成代码。
自底而上分析方法中,LR(0)因没有先行预测,不能处理具有 归约-归约冲突 和 归约-移进冲突 的文法(这里不展开),SLR(1)是用LR(0)的DFA作分析的,比LR(0)更强大但仍有其无法适用的文法规则,LR(1)则是因为DFA状态数太多,导致数据表占用太多内存。LALR(1)是在LR(1)基础上的简化,但会导致出错延迟。综合之下,使用LALR(1)的较为普遍。)
递归下降分析法
以四则运算为例:
BNF文法:
exp
→
exp addop term | term
addop
→
+ | -
term
→
term mulop factor | factor
mulop
→
* | /
factor
→
(exp) | number
消除左递归和左公因子,写出EBNF:
exp
→
term{addop term}
addop
→
+ | -
term
→
factor{mulop factor}
mulop
→
* | /
factor
→
(exp) | number
语法分析代码:
var token;
void main()
{
getToken();//获取下一个(第一个)语法成分赋予变量token,由词法分析提供下一个语法成分
exp();
void exp()
{
term();
while((token=='+')||(token=='-'))
{
match(token);//匹配当前语法成分(token),并获取下一个语法成分赋予token
term();
}
}
void term()
{
factor();
while((token=='*')||(token=='//'))
{
match(token);
factor();
}
}
void factor()
{
switch(token)
{
case'(':
match('(');
exp();
match('(');
break;
case number:
match(number);
break;
default:
error(...);
}
}
void match(expectedToken)
{
if(token==expectedToken)
getToken();//获取下一个符号
else
error(...);
}
至于语义动作插入的位置,要根据实际情况分析进行填入(所以这种分析方法不能由计算机自动生成)
继续此例,填入计算的语义动作后,代码如下:
var token;
var result:double;
void main()
{
getToken();//获取下一个(第一个)语法成分赋予变量token,由词法分析提供下一个语法成分
result=exp();
}
double exp()
{
var addResult:double;
addResult=term();
while((token=='+')||(token=='-'))
{
match(token);//匹配当前语法成分(token),并获取下一个语法成分赋予token
if(token=='+') addResult+=term();
else addResult-=term();
}
return result;
}
double term()
{
var mulResult:double;
mulResult:double=factor();
while((token=='*')||(token=='//'))
{
match(token);
if(token=='*') mulResult*=factor();
else mulResult/=factor();
}
return mulResult;
}
double factor()
{
var factorResult:double;
switch(token)
{
case'(':
match('(');
factorResult=exp();
match('(');
break;
case number:
factorResult=match(number);
break;
default:
error(...);
}
return factorResult;
}
void match(expectedToken)
{
if(token==expectedToken)
getToken();//获取下一个符号
else
error(...);
}
LALR(1):