java开发编译器:自底向上语法解析的基本原理

76 篇文章 10 订阅
55 篇文章 6 订阅

阅读博客的朋友可以到我的网易云课堂中,通过视频的方式查看代码的调试和执行过程:
http://study.163.com/course/courseMain.htm?courseId=1002830012

我开启了新的算法课程:
剑指offer,算法面试技能全面提升指南
http://study.163.com/course/courseMain.htm?courseId=1002942008

在课程中,我将facebook, google, ms,amazon, BAT等公司使用的面试算法题收集起来进行分析,喜欢算法,特别是准备面试,冲击一线互联网公司的朋友不要错过。

自底向上的语法解析,依赖于一种语法格式,我们可称之为LALR(1),跟LL(1)语法类似,LALR语法有以下特点,第二个L表示在解析语法时,从左向右读取语法文本。R表示right most, 也就是在做语法解析时,我们从推导表达式最右边的非终结符开始进行替换解析,LA意思是LOOK AHEAD, 跟LL(1)一样,LALR(1)语法解析时,也需要预先读取输入字符才能做下一步的解析。

LR语法比LL语法更灵活,也更容易实现,正由于它的灵活性,使得在编译器实现中,做语法解析时,使用的都是LALR(1)的解析算法。下面,我们举个例子,看看LALR(1)语法解析的基本原理。

自底向上语法解析的基本原理

自底向上的语法解析算法,通过压栈式自动机,从底向上构建语法解析树。举个具体例子给大家看看:
0. statement -> expr
1. expr -> expr + term
2. | term
3. term -> term * factor
4. | factor
5. factor -> ( expr )
6. | NUM

第一个表达式起始的第一个非终结符statement, 我们称之为全局非终结符。自底向上的语法只能有一个全局非终结符,并且全局非终结符只能出现在一个推导表达式的左边,我们注意到,上面的语法含有左递归,直接使用自顶向下的解析算法是解析不了的。

我们看看,上面的语法,如何通过自底向上的解析算法来识别输入表达式:1 * (2 + 3)。
首先,我们需要一个堆栈,初始时堆栈为空。
stack: input
NULL 1 * (2 + 3)

首先, 我们读取一个输入,把输入对应的token压入堆栈,然后把输入指针指向下一个字符:

NUM * ( 2 + 3)

读取一个输入,把输入对应的token压入堆栈,然后把输入指针指向下一个字符,这个动作,我们称之为shift操作

根据表达式6: factor -> NUM, 此时堆栈上的NUM恰好是它的右边,于是将NUM出栈,然后将factor入栈:

factor * ( 2 + 3)

当堆栈上若干个元素恰好形成某个表达式的右边,算法会将这些元素全部出栈,然后将该表达式对应的左边非终结符入栈,这个动作,我们称之为reduce.

根据表达式4: term -> factor, 于是我们再做一次reduce操作,将factor出栈,term入栈,于是有:
term * ( 2 + 3)

此时堆栈里的term, 构成了表达式2: expr -> term的右边,似乎我们还可以再做一次reduce操作,但我们不能这么做,如果把term 出栈,expr入栈,然而,接下来我们要读入的字符是, 在语法推导中,没有任何表达式右边是包含 expr 这种形式的,所以我们不做reduce操作,于是我们做一个shift操作,读入下一个字符,将他的token压入堆栈,将输入指针指向下一个字符:

term * ( 2 + 3 )

term * 无法构成任何一个推导表达式的右边,所以继续做shift操作:

term * ( 2 + 3 )

堆栈上的元素仍然无法构成任何推导表达式的右边,所以继续做shift操作:

term * ( NUM + 3 )

此时栈顶元素是NUM, 根据表达式6: factor -> NUM, 于是我们可以做一次reduce操作:

term * ( factor + 3 )

根据表达式4: term -> factor, 再做一次reduce操作,于是有:

term * ( term + 3 )

由于下一个要读入的下一个字符是 +, 但是没有任何推导表达式的右边可以包含 term +, 于是我们再做一次reduce操作,(通过预读取下一个字符来决定做shift还是reduce, 这就是语法解析中LA表示的look ahead).
根据表达式2: expr -> term, 我们做reduce操作,于是有:

term * expr + 3 )

由于此时还有输入要处理,所以我们做shift操作:

term * expr + 3 )

再做一次shift :

term * expr + NUM )

根据表达式6: factor -> NUM, 我们可以做一次reduce操作:

term * ( expr + factor )

根据表达式4: term -> factor, 继续做reduce操作:

term * ( expr + term )

此时,堆栈顶端的3个元素构成了表达式1的右边:
expr -> expr + term
的右边,于是做一次reduce操作,将堆栈上三个元素出栈,将expr入栈:

term * ( expr )

此时,我们再做一次shift 操作,将最后一个字符压入堆栈,有:

term * ( expr ) NULL

由于堆栈顶部的3个元素构成表达式 5:
factor -> ( expr )

的右边部分, 于是做一次reduce操作,将堆栈顶部的3个元素出栈,将factor 入栈:

term * factor NULL

此时堆栈上,三个元素构成表达式3:

term -> term * factor

的右边部分,于是做一次reduce操作,将堆栈上三个元素出栈,将term 入栈:

term NULL

根据表达式2: expr -> term, 做reduce操作,将term 出栈, expr 入栈:

expr NULL

根据表达式0: statement -> expr ,做一次reduce操作,于是堆栈变为:

statement NULL

此时,堆栈中含有全局非终结符,此时解析结束,输入的文本可以被语法接受。

由此我们可以总结一下自底向上的解析是如何进行的:

  1. 如果堆栈顶部的若干个元素可以构成某个推导表达式的右边,那么将这几个元素出栈,将表达式左边的非终结符入栈,也就是做一次reduce操作。
  2. 要不然,将当前字符对应的token压入堆栈,同时将输入指针指向下一个字符,也就是做一次shift操作。
  3. 如果做reduce操作后,全局非终结符被压入堆栈,并且输入为空,那么解析结束,输入的文本可以被语法接受。

自底向上的语法解析是如何处理递归情况的

对于自顶向下的解析语法,左递归是不允许的,然而自底向上的解析,完全可以处理语法中出现的递归情况,例如语法:
1. list -> list NUM
2. | NUM

如果输入为123,那么解析堆栈的变化如下:
stack input operation
NULL 123 NULL
NUM 23 shift a NUM
list 23 reduce: list->NUM
list NUM 3 shift a NUM
list 3 reduce: list->list NUM
list NUM NULL shift a NUM
list NULL reduce: list->list NUM

我们看到,堆栈中的元素从来不多过2个,因为每次执行一次shift 操作, NUM 入栈后,我们总能执行一次reduce操作。

我们再看看另一种情况:
1. list -> NUM list
2. | NUM
此时,语法是右递归,我们看看输入为123时,解析堆栈在解析过程中的变化情况:
stack input operation
NULL 123 NULL
NUM 2 3 shift a NUM
NUM NUM 3 shift a NUM
NUM NUM NUM NULL shift a NUM
NUM NUM list NULL reduce, list->NUM
NUM list NULL reduce, list->NUM list
list NULL reduce,list->NUM list

此时,所有的元素都得压入堆栈,reduce操作才可能发生。当语法中出现右递归时,自底向上的解析过程中,必然具备这样的特性,所以在使用自底向上的解析算法时,尽量避免语法中出现右递归的情况。

下一节,我们看看,如何通过代码实现本节所描述的算法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值