java开发编译器:LR 状态机的缺陷与改进

阅读博客的朋友可以到我的网易云课堂中,通过视频的方式查看代码的调试和执行过程:

http://study.163.com/course/courseMain.htm?courseId=1002830012

前两节我们构造的状态机有些缺陷,当我们进入某个状态节点时,根据该节点的特性,我们需要产生一些动作,根据上两节的有限状态机图,当我们进入节点5,我们发现,符号”.”为位于表达式的最右边,在 . 后面不再有其他非终结符或终结符,进入这样的节点时,我们要根据表达式做一次reduce操作,例如在节点5中,表达式和点的情况如下:

t -> f .

此时,我们要根据推导做一次reduce操作,把 f 弹出解析堆栈,把 t 压入解析堆栈,reduce 操作后,状态机的状态从节点5退回到节点0.

同理,如果状态机进入节点3,由于 . 位于推导表达式的最右边,在 . 的后面没有了其他非终结符或终结符,于是要根据节点的推导表达式做reduce操作,根据节点3的表达式:

f -> NUM.

我们需要把 NUM 弹出堆栈,同时将非终结符f压入堆栈,并从节点3 退回到节点0.

问题在于不是每一个节点都有能清晰明确的指定我们分析过程该做什么动作,一个明显的例子是节点1,该节点含有两个表达式,一个表达式中,符号点处于最末尾:

e -> t .

另一个表达式,符号 . 处于表达式的中间:

t -> t . * f

那么像这种情况,我们该做reduce 还是 shift? 这种情况我们称之为 shift / reduce 矛盾。

还有节点11,该节点含有两个表达式,并且符号 . 都在这两个表达式的最后面:

e -> e + t .

t -> t * f .

当处于该节点时,我们到底该依据哪一个表达式做reduce操作?这种情况我们称之为reduce矛盾。

这一节,我们将研究处理这种矛盾的算法。

SLR(1) 语法:

有一个处理 shift / reduce 矛盾的简单方法。还记得LL(1)语法中我们研究过的 FOLLOW set 算法吧。FOLLOW set 指的是,对某个非终结符,根据语法推导表达式构建出的所有可以跟在该非终结符后面的终结符集合,我们称作该非终结符的FOLLOW set.

对于节点4的两个表达式:

e -> t .

t -> t . * f

进入该节点后,到底应该根据第一个表达式做reduce,还是根据第二个表达式做shift操作呢。如果,当前输入字符如果属于 e 的 FOLLOW set, 那么我们就做reduce操作。

上节例子中,根据给定语法,每个非终结符的Follow set 如下:

FOLLOW(s) = {EOI}

FOLLOW(e) = {EOI, },+}

FOLLOW(t) = {EOI, }, + , * }

FOLLOW(f) = {EOI, }, +, * }

对于节点4,如果当前输入字符属于非终结符e的FOLLOW set, 那么就根据表达式:

e -> t.

做reduce 操作,要不然根据另外一个表达式做shift操作。

其他类似节点可根据相同的原则处理,如果语法构建的状态机,出现reduce / shift 矛盾的节点都可以根据上面的原则处理的话,那么这种语法,我们称之为 SLR(1) 语法,SLR 意思是 simple LR 。

LR(1) 语法

有些语法构建的状态机,某些节点的shift / reduce 矛盾可能无法根据上面给出的原则进行处理。当前输入的字符既可以出现在第一个推导表达式左边非终结符的FOLLOW set 中,同时又是第二个表达式 . 右边的终结符,例如上面的节点4,如果 * 属于 e 的 FOLLOW set, 那么shift /reduce 矛盾就难以解决了。

事实上,我们不需要考虑当前输入字符是否属于非终结符的FOLLOW set, 只要考虑,当前输入字符,当我们做了reduce操作后,在状态机当前上下文环境下,是否能合法的跟在reduce后的非终结符的后面。

这种能合法的跟在某个非终结符后面的符号集合,我们称之为look ahead set, 它是FOLLOW set 的子集。

结合具体实例,我们看看 look ahead set 是怎样的。对于状态节点 11, 它是一个 shift / reduce 矛盾 节点。从节点0到节点11的路径有两条:

0 -> 2 -> 9 -> 11

0 -> 4 -> 8 -> 9 -> 11

要收集 e 的 look ahead 集合,我们只要收集这两条路径上,跟在 e 后面的终结符就可以了。当我们做reduce操作后,相应的非终结符会被压入堆栈,一旦该非终结符被压入解析堆栈,能跟在该非终结符后面的终结符最好要对应于下即将要输入的字符。我们看节点0,如果解析过程中,产生一个生成 e 的reduce 回到节点0,例如:

(0, NUM) -> 3 –(reduce by f -> NUM.) -> 0

(0, f) -> 5 – (reduce by t -> f.) -> 0

(0, t) -> 1 – (reduce by e -> t.) -> 0

此时,在节点0以及当前输入 e 跳转到节点2。

根据表达式:

e -> e . + t

由于 . 后面跟着 +, 我们希望进入节点2 后,当前输入的字符就是 +, 或者根据另一个表达式:

s -> e .

我们也可以期望,下一个输入字符是EOI, 这样我们的

look ahead 集合就包含 EOI, +.

也就是说,如果状态机根据路径:

0 -> 2 -> 9 -> 11

进入状态节点11, 并且进入节点11后,当前输入是EOI, + 的话,我们可以做reduce操作,退回到节点0.

再看节点4中的表达式:

f -> ( . e )

跟在e 后面的终结符是 ), 这表明如果进入到节点11,并且输入是字符 ), 时,我们可以做reduce 操作,同时根据路径:0 -> 4 -> 8 -> 9 -> 11 返回到节点4,并且e也会被压入解析堆栈,这样根据 (4, e) 可以进入节点8,然后根据(8, ) ) 可以进入节点 10.

由此 e 的 look ahead 集合就包含 EOI, + , ), 在节点11,决定是否要做reduce ,只要看下一个输入的字符是否属于e的look ahead 集合就可以了。

这样一来,每个节点中的每个表达式,都需要配备一个对应的 look ahead 集合,如果两个表达式相同,但他们的look ahead 集合不同,那么,我们就认为这两个表达式也是不同的。假定一个表达式结合它对应的look ahead 如下:

s -> α .x β, C

x -> . r

C 是 look ahead 集合,α, β, r 是0个或多个终结符或非终结符的组合, x 是非终结符,那么 x -> . r 的look ahead 集合如下:

FIRST(β C),就是 FIRST(β) 并上 C.

创建带有look ahead 集合表达式的过程主要在closure阶段进行。举个例子,对于节点0中的表达式:

s -> . e

我们先给他初始化一个look ahead 集合为 {EOI}.于是有:

s -> α .x β, C

s -> .e , {EOI}

根据:

x -> . r FIRST(β C)

我们有:

e -> .e + t FIRST(β C)

e -> . t FIRST(β C)

由于β 是空,因此 FIRST(β C) = C = {EOI},于是就有:

e-> .e + t {EOI}

e-> .t {EOI}

得到上面的表达式后,我们继续往下推:

s -> α .x β, C

e-> .e + t {EOI}

根据 :

x -> .r FIRST(β C)

e -> .e + t

由于β = + t, 于是

FIRST(β) = +

FIRST(β C) = {+, EOI}

因此有新的表达式:

e -> .e + t {+, EOI}

这个过程反复进行,直到没有新的表达式可以生成为止。

我们根据节点0,将这个算法的具体实现走一遍,为了实现上面的算法,我们需要两个数据结构,一个是堆栈productionStack, 一个是集合 closureSet.

首先,我们初始化节点0的表达式,并给表达式赋予一个初始的look ahead 集合 {EOI}:

[s -> . e {EOI}]

将上面的表达式压入堆栈以及集合:

productionStack:

[s -> . e {EOI}]

closureSet:

[s -> . e {EOI}]

如果堆栈不为空,将堆栈顶部的表达式出栈,并利用上面讲到的算法构造新的表达式,此时栈顶的表达式为:

[s -> . e {EOI}]

根据:

s -> α .x β, C

s-> .e , {EOI}

x -> . r FIRST(β, C)

我们可以产生两个新的表达式:

[e -> .e + t {EOI}]

[e -> . t {EOI}]

分别将他们压入堆栈和加入集合:

productionStack:

[e -> . t {EOI}]

[e -> .e + t {EOI}]

closureSet:

[s -> . e {EOI}]

[e -> .e + t {EOI}]

[e -> . t {EOI}]

把堆栈顶部的表达式出栈:

[e -> . t {EOI}]

构造新的表达式:

[t -> .t * f {EOI}]

[t -> .f {EOI}]

将他们加入堆栈和集合:

productionStack:

[t -> .f {EOI}]

[t -> .t * f {EOI}]

[e -> .e + t {EOI}]

closureSet:

[s -> . e {EOI}]

[e -> .e + t {EOI}]

[e -> . t {EOI}]

[t -> .t * f {EOI}]

[t -> .f {EOI}]

将栈顶表达式出栈:

[t -> .f {EOI}]

同理构造新的表达式:

[f ->. ( e ) {EOI}]

[f -> . NUM {EOI}]

将新生成的表达式压入堆栈和加入集合:

productionStack:

[f -> . NUM {EOI}]

[f ->. ( e ) {EOR}]

[t -> .t * f {EOI}]

[e -> .e + t {EOI}]

closureSet:

[s -> . e {EOI}]

[e -> .e + t {EOI}]

[e -> . t {EOI}]

[t -> .t * f {EOI}]

[t -> .f {EOI}]

[f ->. ( e ) {EOR}]

[f -> . NUM {EOI}]

将堆栈顶部的表达式出栈:

[f -> . NUM {EOI}]

由于. 后面不是非终结符,所以对该表达式不做处理。继续弹出栈顶表达式:

[f ->. ( e ) {EOI}]

同理,由于.后面不是非终结符,所以对该表达式不做处理。继续弹出栈顶表达式:

[t -> .t * f {EOI}]

此时,β 对应的部分为 * f, 所以FIRST(β C) = FIRST(* f) 并上 {EOI}, 也就是{* EOI},于是我们生成新的表达式:

[t -> . t * f, {* EOI}]

[t -> . f {* EOI}]

将上面表达式压入堆栈和加入集合:

productionStack:

[t -> . f {* EOI}]

[t -> . t * f, {* EOI}]

[e -> .e + t {EOI}]

closureSet:

[s -> . e {EOI}]

[e -> .e + t {EOI}]

[e -> . t {EOI}]

[t -> .t * f {EOI}]

[t -> .f {EOI}]

[f ->. ( e ) {EOR}]

[f -> . NUM {EOI}]

[t -> . t * f, {* EOI}]

[t -> . f {* EOI}]

大家注意看,表达式

1.[t -> . t * f, {* EOI}] 和

2.[t -> .t * f {EOI}]

唯一的区别在于 look ahead 集合不同,第一个表达式的look ahead 集合比第二个表达式要大,我们称这种情况为表达式1 覆盖了表达式2,有了表达式1,表达式2就不必要了,因此我们把表达式2从集合里删除,同理表达式

[t -> .f {EOI}]

也要被删除,这样

closureSet为:

[s -> . e {EOI}]

[e -> .e + t {EOI}]

[e -> . t {EOI}]

[f ->. ( e ) {EOR}]

[f -> . NUM {EOI}]

[t -> . t * f, {* EOI}]

[t -> . f {* EOI}]

接下来继续弹出栈顶表达式:

[t -> . f {* EOI}]

生成新的表达式:

[f -> .( e ) {* EOI}]

[f -> .NUM {* EOI}]

将他们压入堆栈有:

productionStack:

[f -> .NUM {* EOI}]

[f -> .( e ) {* EOI}]

[t -> . t * f, {* EOI}]

[e -> .e + t {EOI}]

由于生成的表达式覆盖了closureSet中原有的表达式:

[f ->. ( e ) {EOR}]

[f -> . NUM {EOI}]

将上面两个表达式从集合中删除,并将新生成的表达式加入集合:

closureSet:

[s -> . e {EOI}]

[e -> .e + t {EOI}]

[e -> . t {EOI}]

[t -> . t * f, {* EOI}]

[t -> . f {* EOI}]

[f -> .( e ) {* EOI}]

[f -> .NUM {* EOI}]

此时栈顶两个表达式分别是:

[f -> .NUM {* EOI}]

[f -> .( e ) {* EOI}]

这两个表达式中,.右边不是非终结符,所以直接将他们出栈。继续弹出栈顶表达式:

[t -> . t * f, {* EOI}]

由于β 对应 * f, FIRST(β) = {},因此生成的look ahead 集合仍然是 { EOI}, 生成新的表达式为:

[t -> . t * f {* EOI}]

[t -> . f {* EOI}]

这两个表达式已经在集合中存在,所以不再加入集合。

继续弹出栈顶表达式:

[e -> .e + t {EOI}]

β 对应的是 + t, FIRST(β) = FIRST(+ t) = {+}, 于是生成新的表达式时对应的look ahead 集合是 {+ EOI},于是有:

[e -> . e + t {+ EOI}]

[e ->. t {+ EOI}]

分别将他们压入堆栈:

productionStack:

[e ->. t {+ EOI}]

[e -> . e + t {+ EOI}]

由于这两个表达式覆盖了集合中的表达式:

[e -> . t {EOI}]

[e -> .e + t {EOI}]

所以讲上面两个表达式从集合中删除,将新表达式加入集合:

closureSet:

[s -> . e {EOI}]

[t -> . t * f, {* EOI}]

[t -> . f {* EOI}]

[f -> .( e ) {* EOI}]

[f -> .NUM {* EOI}]

[e -> . e + t {+ EOI}]

[e ->. t {+ EOI}]

将栈顶表达式出栈:

[e ->. t {+ EOI}]

生成新的表达式:

[t -> . t * f {+ EOI}]

[t -> .f {+ EOI}]

将新生成的表达式压入堆栈同时加入集合:

productionStack:

[t -> .f {+ EOI}]

[t -> . t * f {+ EOI}]

[e -> . e + t {+ EOI}]

closureSet:

[s -> . e {EOI}]

[t -> . t * f, {* EOI}]

[t -> . f {* EOI}]

[f -> .( e ) {* EOI}]

[f -> .NUM {* EOI}]

[e -> . e + t {+ EOI}]

[e ->. t {+ EOI}]

[t -> . t * f {+ EOI}]

[t -> .f {+ EOI}]

弹出栈顶表达式:

[t -> .f {+ EOI}]

生成新的表达式:

[f -> . ( e ) {+ EOI}]

[f -> . NUM {+ EOI} ]

将新表达式压入堆栈和加入集合:

productionStack:

[f -> . NUM {+ EOI} ]

[f -> . ( e ) {+ EOI}]

[t -> . t * f {+ EOI}]

[e -> . e + t {+ EOI}]

closureSet:

[s -> . e {EOI}]

[t -> . t * f, {* EOI}]

[t -> . f {* EOI}]

[f -> .( e ) {* EOI}]

[f -> .NUM {* EOI}]

[e -> . e + t {+ EOI}]

[e ->. t {+ EOI}]

[t -> . t * f {+ EOI}]

[t -> .f {+ EOI}]

[f -> . ( e ) {+ EOI}]

[f -> . NUM {+ EOI} ]

由于堆栈顶部的两个表达式, . 右边都不是非终结符,因此将他们直接出栈。

继续弹出栈顶表达式:

[t -> . t * f {+ EOI}]

此时β 对应的是 * f, FIRST(β) = FIRST(* f) = {},于是新生成的表达式对应的look ahead 集合为 { + EOI}:

[t -> . t * f {* + EOI}]

[t -> . f {* + EOI}]

将表达式压入堆栈

productionStack:

[t -> . f {* + EOI}]

[t -> . t * f {* + EOI}]

[e -> . e + t {+ EOI}]

注意到,新生成的表达式:

[t -> . t * f {* + EOI}]

它覆盖集合中原有表达式:

[t -> . t * f {* EOI}]

[t -> . t * f {+ EOI}]

[t -> . f {* + EOI}]

覆盖了:

[t-> . f {* EOI}]

[t -> . f {+ EOI} ]

删除被覆盖的表达式,加入新表达式

closureSet:

[s -> . e {EOI}]

[f -> .( e ) {* EOI}]

[f -> .NUM {* EOI}]

[e -> . e + t {+ EOI}]

[e ->. t {+ EOI}]

[f -> . ( e ) {+ EOI}]

[f -> . NUM {+ EOI} ]

[t -> . t * f {* + EOI}]

[t -> . f {* + EOI}]

弹出栈顶表达式:

[t -> . f {* + EOI}]

构造新的表达式:

[f -> . ( e ) {* + EOI}]

[f -> . NUM {* + EOI}]

将他们压入堆栈:

productionStack:

[f -> . NUM {* + EOI}]

[f -> . ( e ) {* + EOI}]

[t -> . t * f {* + EOI}]

[e -> . e + t {+ EOI}]

由于表达式

[f -> . ( e ) {* + EOI}]

覆盖集合中的表达式:

[f -> .( e ) {* EOI}]

[f -> .( e ) {+ EOI}]

以及

[f -> . NUM {* + EOI}]

覆盖集合中原有表达式:

[f -> .NUM {* EOI}]

[f -> .NUM {+ EOI}]

删除被覆盖的表达式,加入新表达式后

closureSet:

[s -> . e {EOI}]

[e -> . e + t {+ EOI}]

[e ->. t {+ EOI}]

[t -> . t * f {* + EOI}]

[t -> .f {+ EOI}]

[f -> . ( e ) {* + EOI}]

[f -> . NUM {* + EOI}]

此时栈顶两表达式, . 右边的符号不是非终结符,将栈顶两个表达式出栈。

接着继续弹出栈顶表达式:

[t -> . t * f {* + EOI}]

生成表达式:

[t -> .t * f {* + EOI}]

[t -> .f {* + EOI}]

两个表达式已经在集合中存在,于是不做处理。

继续弹出栈顶表达式:

[e -> . e + t {+ EOI}]

它生成两个表达式

[e -> .e + t {+ EOI}]

[e -> . t {+ EOI}]

由于表达式都存在集合中,于是不做处理。到此堆栈为空,整个closure流程结束,此时整个closure集合为:

[s -> . e {EOI}]

[e -> . e + t {+ EOI}]

[e ->. t {+ EOI}]

[f -> . ( e ) {+ EOI}]

[t -> . t * f {* + EOI}]

[t -> . f {* + EOI}]

[f -> . ( e ) {* + EOI}]

[f -> . NUM {* + EOI}]

除了closure 这个步骤改变外,节点生成的其他步骤都保持不变。

时间: 2024-08-27 10:15:22

java开发编译器:LR 状态机的缺陷与改进的相关文章

atitit.自己动手开发编译器and解释器(1) ------词法分析--attilax总结

atitit.自己动手开发编译器and解释器(1) ------词法分析--attilax总结 1.   应用场景:::DSL 大大提升开发效率 1 2. 2. 流程如下::: 词法分析(生成token流) >>>>语法分析(生成ast) >>解释执行... 2 3. 如何进行词法分析?Fsm状态机(自动机) 2 4. 使用状态模式构建FSM  (简单,易用..推荐首选) 2 5. ---代码( 状态模式构建FSM ) 3 6. 词法分析概念 3 6.1. 词法分析(英

atitit.自己动手开发编译器and解释器(2) ------语法分析,语义分析,代码生成--attilax总结

atitit.自己动手开发编译器and解释器(2) ------语法分析,语义分析,代码生成--attilax总结 1. 建立AST 抽象语法树 Abstract Syntax Tree,AST) 1 2. 建立AST 语法树----递归下降(recursive descent)法 2 3. 语法分析概念 2 3.1. 上下文无关语言,非终结符(nonterminal symbol),终结符(terminal symbol).注 2 3.2. 最左推导.当然也有最右推导 3 3.3. 分支预测的

Java开发工具

每一个程序员都有一个自己的开发工具,使用得心应手,好的工具能做到事半功倍效果.但人与人不同,喜好不同,使用的工具也不同.就像侠客需要武器,十八般兵器,各有所长,每个侠客都有属于自己的兵器.下面介绍下IT界的兵器们: JDK (Java Development Kit)Java开发工具集 .Eclipse.MyEclipse.UltraEdit.Java Workshop.NetBeans 与Sun Java Studio 5.Borland 的JBuilder.Oracle 的JDevelope

阿里巴巴Java开发手册1.4.0

转自官网 前言 <阿里巴巴Java开发手册>是阿里巴巴集团技术团队的集体智慧结晶和经验总结,经历了多次大规模一线实战的检验及不断完善,系统化地整理成册,回馈给广大开发者.现代软件行业的高速发展对开发者的综合素质要求越来越高,因为不仅是编程知识点,其它维度的知识点也会影响到软件的最终交付质量.比如:数据库的表结构和索引设计缺陷可能带来软件上的架构缺陷或性能风险:工程结构混乱导致后续维护艰难:没有鉴权的漏洞代码易被黑客攻击等等.所以本手册以Java开发者为中心视角,划分为编程规约.异常日志.单元测

java开发系统内核:让内核从严重错误中恢复

更详细的讲解和代码调试演示过程,请参看视频 用java开发C语言编译器 更详细的讲解和代码调试演示过程,请参看视频 如何进入google,算法面试技能全面提升指南 如果你对机器学习感兴趣,请参看一下链接: 机器学习:神经网络导论 更详细的讲解和代码调试演示过程,请参看视频 Linux kernel Hacker, 从零构建自己的内核 微软早期的DOS系统,存在一个严重的问题是,如果应用程序运行出现问题,它会导致整个系统完全奔溃掉,我们当前的系统内核也存在这一的问题,例如打开api_call.as

Unit01: JAVA开发环境案例

Top JAVA Fundamental DAY01 JDK及Eclipse目录结构操作 JDK的安装及配置 控制台版的JAVA HelloWorld 使用Eclipse开发Java应用程序 1 JDK及Eclipse目录结构操作 1.1 问题 为熟练掌握 Linux 下的目录操作,本案例需要完成如下操作: 在Linux系统下,浏览jdk的目录结构. 在Linux系统下,浏览eclipse的目录结构. 1.2 方案 完成此案例,需要用到一些常用的 Linux命令.这些命令如下所示: pwd :显

Java开发和运行环境的搭建

Java开发需要准备的东西? JDK+Eclipse 其中JDK的意思是Java开发工具包,Eclipse是进行用于做Java程序开发的工具(当然你也可以用记事本什么的去做). 其他开发工具:JCreator,JBuilder,... jdk的介绍和安装教程度娘里面到处都是,这里自己也在啰嗦一下吧. 关于jdk的详细介绍: JDK是Java Development Kit的缩写,即Java开发工具集.JDK是整个Java的核心,包括了Java运行环境(JRE).Java开发工具和Java基础类库

「3」Java开发环境搭建

1.JRE和JDK的概念 ●什么是JRE和JDK? ●JRE(Java Runtime Environment-Java运行环境).所有的Java程序都要在JRE下才能运行.它包括了JVM(Java Virtual Machine)和JAVA核心类库等. ●JDK(Java Development Kit-Java开发工具包).它是整个Java的核心.包括了Java运行环境.Java开发工具(javac.java.javadoc等)和Java基础类库. 总结:与JDK相比,JRE它不包含开发工具

Ma下java开发环境搭建

Mac下Java开发环境搭建 mac配置java开发环境:eclipse + jdk1.8 前言 搭建开发环境之前,先了解一下一些概念:java,javaSE,javaEE,JVM,JDK,JRE java发展历程 JavaSE.JavaME.JavaEE三者之间的关系JavaSE(J2SE):(Java2 Platform Standard Edition,java平台标准版)JavaEE(J2EE):(Java 2 Platform,Enterprise Edition,java平台企业版)