语法分析

两个基本功:第一,必须能够阅读和书写语法规则,也就是掌握上下文无关文法;第二,必须要掌握递归下降算法。

两种算法思路:一种是自顶向下的语法分析,另一种则是自底向上的语法分析。

上下文无关文法(Context-Free Grammar)


start:blockStmts ;               //起始
block : '{' blockStmts '}' ;      //语句块
blockStmts : stmt* ;              //语句块中的语句
stmt = varDecl | expStmt | returnStmt | block;   //语句
varDecl : type Id varInitializer? ';' ;         //变量声明
type : Int | Long ;                              //类型
varInitializer : '=' exp ;                       //变量初始化
expStmt : exp ';' ;                              //表达式语句
returnStmt : Return exp ';' ;                    //return语句
exp : add ;                                      //表达式       
add : add '+' mul | mul;                         //加法表达式
mul : mul '*' pri | pri;                         //乘法表达式
pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式 

 

在语法规则里,我们把冒号左边的叫做非终结符(Non-terminal),又叫变元(Variable)

非终结符可以按照右边的正则表达式来逐步展开,直到最后都变成标识符、字面量、运算符这些不可再展开的符号,也就是终结符(Terminal)

终结符其实也是词法分析过程中形成的 Token。

像这样左边是非终结符,右边是正则表达式的书写语法规则的方式,就叫做扩展巴科斯范式(EBNF)。ANTLR 这样的语法分析器生成工具中,经常会看到这种格式的语法规则。

 

另一种写法,就是产生式(Production Rule),又叫做替换规则(Substitution Rule)。产生式的左边是非终结符(变元),它可以用右边的部分替代,中间通常会用箭头连接。

 

 

总结起来,语法规则是由 4 个部分组成的:一个有穷的非终结符(或变元)的集合;一个有穷的终结符的集合;一个有穷的产生式集合;一个起始非终结符(变元)。那么符合这四个特点的文法规则,就叫做上下文无关文法(Context-Free Grammar,CFG)。

正则文法是上下文无关文法的一个子集。其实,正则文法也可以写成产生式的格式。比如,数字字面量(正则表达式为“[0-9]+”)可以写成:

IntLiteral -> Digit IntLiteral1IntLiteral1 -> Digit IntLiteral1 IntLiteral1 -> εDigit -> [0-9]

 

在上下文无关文法里,产生式的右边可以放置任意的终结符和非终结符,而正则文法只是其中的一个子集,叫做线性文法(Linear Grammar)。它的特点是产生式的右边部分最多只有一个非终结符,比如 X->aYb,其中 a 和 b 是终结符。

 

在高级语言里,本地变量必须先声明,才能在后面使用。这种制约关系就是上下文相关的。不过,在语法分析阶段,我们一般不管上下文之间的依赖关系,这样能使得语法分析的任务更简单。而对于上下文相关的情况,则放到语义分析阶段再去处理。

 

递归下降算法(Recursive Descent Parsing)

总结起来,递归下降算法的特点是:对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回退回来,尝试另一个产生式。这种回退过程,叫做回溯(Backtracking)。

 

 

递归下降算法,能非常有效地处理很多语法规则,但是它也有两个缺点。

第一个缺点,就是著名的左递归(Left Recursion)问题。比如,在匹配算术表达式时,产生式的第一项就是一个非终结符 add,那么按照算法,要下降一层,继续匹配 add。这个过程会一直持续下去,无限递归下去。

 

把产生式改成右递归不就可以了吗?也就是 add 这个递归项在右边:

add -> mul + add

这样确实可以避免左递归问题,但它同时也会导致结合性的问题。

2+3+4这个表达式

它会先计算“3+4”,而不是先计算“2+3”。这破坏了加法的结合性规则,加法运算本来应该是左结合的。

 

标准的方法,能避免左递归问题,可以改写原来的语法规则,也就是引入add',把左递归变成右递归。

add -> mul add'

add' -> + mul add' | ε

这种改写方法虽然能够避免左递归问题,但由于add'的规则是右递归的,采用标准的递归下降算法,仍然会出现运算符结合性的错误。

 

把递归调用转化成循环,解决左递归问题。


add : mul ('+' mul)*  ;

对于('+'mul)*这部分,我们其实可以写成一个循环。而在循环里,我们可以根据结合性的要求,手工生成正确的 AST。

 

 

递归下降算法的第二个缺点,就是当产生式匹配失败的时候,必须要“回溯”,这就可能导致浪费。

有个针对性的解决办法,就是预读后续的一个 Token,判断该选择哪个产生式。

 

自动计算出选择不同产生式的依据的算法,LL 算法家族。

LL 算法:计算 First 和 Follow 集合

LL(1) 中的第一个 L,是 Left-to-right 的缩写,代表从左向右处理 Token 串。第二个 L,是 Leftmost 的缩写,意思是最左推导。最左推导是什么呢?就是它总是先把产生式中最左侧的非终结符展开完毕以后,再去展开下一个。这也就相当于对 AST 从左子节点开始的深度优先遍历。LL(1) 中的 1,指的是预读一个 Token。

 

递归下降和 LL 算法,都是自顶向下的算法。还有一类算法,是自底向上的,其中的代表就是 LR 算法。

LR 算法:移进和规约

而 LR 算法的原理呢,则是从底下先拼凑出 AST 的一些局部拼图,并逐步组装成一棵完整的 AST。所以,其中的关键之处在于如何“拼凑”。

LR 算法。L 还是代表从左到右读入 Token,而 R 是最右推导(Rightmost)的意思。

LR(k),那它的意思就是会预读 k 个 Token。

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值