首页 > 编程语言 >java开发编译器:LR 状态机的缺陷与改进

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

时间:2023-06-14 11:06:39浏览次数:44  
标签:java EOI reduce 状态机 编译器 NUM 终结符 节点 表达式

前两节我们构造的状态机有些缺陷,当我们进入某个状态节点时,根据该节点的特性,我们需要产生一些动作,根据上两节的有限状态机图,当我们进入节点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 这个步骤改变外,节点生成的其他步骤都保持不变。


标签:java,EOI,reduce,状态机,编译器,NUM,终结符,节点,表达式
From: https://blog.51cto.com/u_16160261/6476211

相关文章

  • java开发C编译器:结构体的解析和执行
    更详细的讲解和代码调试演示过程,请参看视频用java开发C语言编译器结构体是C语言中,最为复杂的原生数据结构,它把多种原生结构结合在一起,形成一个有特点含义的数据结构,要实现一个完整的C语言编译器或解释器,就必须要拥有对结构体的解析能力,本节,我们在当前解释器的基础上,增加结构体的解......
  • 用java做操作系统内核:软盘读写
    在前两节,我们将一段代码通过软盘加载到了系统内存中,并指示cpu执行加入到内存的代码,事实上,操作系统内核加载也是这么做的。只不过我们加载的代码,最大只能512byte,一个操作系统内核,少说也要几百兆,由此,系统内核不可能直接从软盘读入系统内存。通常的做法是,被加载进内存的512Byte程......
  • java开发系统内核:caps 按键处理
    更详细的讲解和代码调试演示过程,请参看视频LinuxkernelHacker,从零构建自己的内核上一节,我们成功实现了对shift按键的处理,这一节,我们看看如何处理caps按键,当该键按下时,输入系统的字符在大小写间切换。由于我们系统启动后,默认输入是大写字符,完成本节后,我们把系统的默认字符改成......
  • java开发系统内核:像Linux一样使用中断实现内核API
    我们当前提供的内核API有个问题,就是每次使用时,需要计算API函数在内核中的位置,一旦内核代码改变,API接口的位置也会改变,同时调用API的应用程序也必须跟着改变,显然这种限制是不可接受的。为了突破当前缺陷,我们必须想出新的API提供办法。常用的做法是,仿照Linux将API当做一个中断调用,由......
  • java开发系统内核:使用C语言开发系统应用程序
    更详细的讲解和代码调试演示过程,请参看视频用java开发C语言编译器更详细的讲解和代码调试演示过程,请参看视频如何进入google,算法面试技能全面提升指南如果你对机器学习感兴趣,请参看一下链接:机器学习:神经网络导论更详细的讲解和代码调试演示过程,请参看视频LinuxkernelHacker,......
  • java开发C语言编译器:JVM 的基本操作指令介绍及其程序运行原理
    更详细的讲解和代码调试演示过程,请参看视频用java开发C语言编译器更详细的讲解和代码调试演示过程,请参看视频如何进入google,算法面试技能全面提升指南如果你对机器学习感兴趣,请参看一下链接:机器学习:神经网络导论更详细的讲解和代码调试演示过程,请参看视频LinuxkernelHacker,......
  • 编译原理动手实操,用java实现一个简易编译器-语法解析
    语法和解析树:举个例子看看,语法解析的过程。句子:“我看到刘德华唱歌”。在计算机里,怎么用程序解析它呢。从语法上看,句子的组成是由主语,动词,和谓语从句组成,主语是“我”,动词是“看见”,谓语从句是”刘德华唱歌“。因此一个句子可以分解成主语+动词+谓语从句:句子-->主语+动词+谓语......
  • java开发系统内核:进程的挂起和恢复
    有了进程的自动调度后,接下来的任务在于,如何将空闲进程挂起,空闲进程往往是那些没有具体任务需要处理的进程,因此,如果继续让其运行的话,那么必然会耗费宝贵的CPU资源,如果能让它先挂起,等到它需要执行具体任务时,再把它调度到前台,那才是一种合理的进程管理机制。我们实现的进程调度,是依赖......
  • java开发C语言编译器: return 语句的解释和执行
    在C语言程序中,很多函数并不是执行全部语句后,才从最底部返回的,大多数情况下,当某些条件成立时就可以通过return语句立即返回,而无需执行接下来的代码,本节,我们继续增强java开发的C语言解释器功能,使其能够处理return语句,完成本节代码后,我们的C语言解释器能够正常解析和执行下面的代码:in......
  • java开发系统内核:自动化进程切换
    我们已经通过时钟中断完成了两个进程间的相互切换。但当前实现有很大的缺陷,例如我们只能在两个指定的进程间切换,如果要想增添新的进程,那么,没增加一个进程,按照当前模式,我们只能再增加相应代码,这显然是不可接受的。因此,这节,我们希望完成进程的切换机制,使得有新进程时,我们无需改动代码......