系列导航

  1. (一)语法分析介绍
  2. (二)LR(0) 语法分析
  3. (三)LALR 语法分析
  4. (四)二义性文法
  5. (五)错误恢复
  6. (六)构造语法分析器

首先,需要介绍下 LALR 语法分析的基础:LR(0) 语法分析。

还是以之前的算式文法为例:

$E \to E + T$

$E \to T$

$T \to T * F$

$T \to F$

$F \to id$

$F \to (E)$

先来看一下 $(id+id)$ 是如何被 LR(0) 语法分析执行的。这里使用 $\$$ 这个特殊符号来标记输入的结束。

输入 动作
$(id_1+id_2)\$$ 移入
$($ $id_1+id_2)\$$ 移入
$(id_1$ $+id_2)\$$ 按照 $F \to id$ 归约
$(F$ $+id_2)\$$ 按照 $T \to F$ 归约
$(T$ $+id_2)\$$ 按照 $E \to T$ 归约
$(E$ $+id_2)\$$ 移入
$(E+$ $id_2)\$$ 移入
$(E+id_2$ $)\$$ 按照 $F \to id$
$(E+F$ $)\$$ 按照 $T \to F$
$(E+T$ $)\$$ 按照 $E \to E + T$
$(E$ $)\$$ 移入
$(E)$ $\$$ 按照 $F \to (E)$ 归约
$F$ $\$$ 按照 $T \to F$ 归约
$T$ $\$$ 按照 $E \to T$ 归约
$E$ $\$$ 接受

可以看到,LR(0) 语法分析会不断将输入的符号移入到栈中,如果栈里的符号是某个产生式的右部,就会弹出栈内符号并归约为其头部,再将头部符号入栈,直到找到起始非终结符,接受并完成语法分析。

每次都去比较栈里的符号和所有产生式,也可以完成语法分析,但显然这样太过低效,实际使用中会构造出 LR(0) 自动机,利用 LR 语法分析表来提高匹配效率。

一、项和 LR(0) 自动机

LR(0) 语法分析器会通过维护一些状态,来表明我们在语法分析过程中所处的位置,从而决定现在需要移入还是归约。

LR(0) 使用“项”(item)来表示现在已经看到了产生式的哪些部分。项是由产生式再加上一个位于它的右部中某处的点组成的。例如产生式 $A \to XYZ$ 产生了四个项:

$$\begin{matrix}

A \to \cdot \ XYZ \\

A \to X \cdot YZ \\

A \to XY \cdot Z \\

A \to XYZ\ \cdot \ \\

\end{matrix}$$

例如,项 $A \to \cdot \ XYZ$ 表示我们希望在接下来的输入中看到一个从 $XYZ$ 推导得到的串。项 $A \to X \cdot YZ$ 表示我们刚刚在输入中看到了一个可以由 $X$ 推导得到的串,并且我们希望接下来看到一个能从 $YZ$ 推导的串。项 $A \to XYZ\ \cdot \ $ 表示我们已经看到了产生式体 $XYZ$,已经是时候把 $XYZ$ 归约为 $A$ 了。

LR(0) 语法分析器的状态,就是这样的项的集合(或者称为“项集”),因此可以用于决定现在需要移入还是归约。这些状态的集合(或者称为“项集族”)就可以构造出 LR(0) 自动机,自动机的状态就对应一个项集。

二、构造 LR(0) 自动机

为了构造 LR(0) 自动机,首先定义一个增广文法(augmented grammar),如果 $G$ 是一个以 $S$ 为开始符号的文法,那么它的增广文法 $G'$ 就是在 $G$ 中加上新的开始符号 $S'$ 和产生式 $S' \to S$ 而得到的文法。

引入新的开始符号的目的是告诉语法分析器何时应该停止语法分析并接受输入符号串,当且仅当使用产生式 $S' \to S$ 进行归约时,输入符号串被接受。

上面算式文法对应的增广文法如下所示:

$0.\ E' \to E$

$1.\ E \to E + T$

$2.\ E \to T$

$3.\ T \to T * F$

$4.\ T \to F$

$5.\ F \to id$

$6.\ F \to (E)$

然后,需要两个函数 $\text{CLOSURE}$(闭包) 和 $\text{GOTO}$。

项集的闭包

如果 $I$ 是文法 $G$ 的一个项集,那么 $\text{CLOSURE}(I)$ 就是能够从 $I$ 的定点右侧继续推导时可能用到的所有产生式对应的项。

构造闭包的方法很简单:

  1. 首先 $\text{CLOSURE}(I)$ 只包含 $I$ 本身
  2. 如果 $A \to \alpha \cdot B \beta$ 在 $\text{CLOSURE}(I)$ 中,且 $B \to \gamma$ 是一个产生式,且项 $B \to \cdot \gamma$ 不在 $\text{CLOSURE}(I)$ 中,那么就将这个项添加到闭包中。不断应用这个规则,直到没有新项可以添加到 $\text{CLOSURE}(I)$ 中为止。

还是以之前的算式文法为例,其增广文法的项 $E' \to \cdot E$ 对应的闭包如下所示:

$E' \to \cdot E$

$E \to \cdot E+T$

$E \to \cdot T$

$T \to \cdot T*F$

$T \to \cdot F$

$F \to \cdot id$

$F \to \cdot (E)$

其计算过程为:

  • 根据规则 1,将 $E' \to \cdot E$ 加入闭包。
  • 根据规则 2,定点右侧包含 $E$,因此将 $E$ 的产生式的项(定点位于最左端)$E \to \cdot E+T$ 和 $E \to \cdot T$ 加入闭包。
  • 现在定点右侧包含 $T$,因此将 $T$ 的产生式的项 $T \to \cdot T*F$ 和 $T \to \cdot F$ 加入闭包。
  • 现在定点右侧包含 $F$,因此将 $F$ 的产生式的项 $F \to \cdot id$ 和 $F \to \cdot (E)$ 加入闭包。
  • 现在定点右侧没有更多非终结符,过程终止。

该算法的具体实现可以参见这里

对于闭包,还可以进一步划分为如下两类:

  • 内核项:包含初始项 $S' \to \cdot S$ 和所有定点不在最左端的项。
  • 非内核项:除了初始项 $S' \to \cdot S$ 意外所有定点在最左端的项。

在上面的例子中,只有 $E' \to \cdot E$ 是内核项,其它的都是非内核项。或者说,在计算 $\text{CLOSURE}(I)$ 时,只有 $I$ 是内核项,其它后加入的都是非内核项。

这样区分的原因,是在生成语法分析器的过程中,只有内核项需要一直保存在内存中,非内核项只需要在使用时临时计算出来即可,可以有效减少不必要的内存占用。

GOTO 函数

接下来就是另一个函数 $GOTI(I, X)$ 了,其中 $I$ 是一个项集,$X$ 是一个符号(终结符或非终结符)。$\text{GOTO}(I, X)$ 表示了项集 $I$ 中所有形如 $A \to \alpha \cdot X \beta$ 的项所对应的 $ \to \alpha X \cdot \beta$ 的闭包。由于项集对应了 LR(0) 自动机中的状态,$\text{GOTO}(I,X)$ 就表示了自动机中的状态 $I$ 在看到输入 $X$ 后,需要转换到的新状态。

拿上面 $E' \to \cdot E$ 的闭包为例:

$E' \to \cdot E$

$E \to \cdot E+T$

$E \to \cdot T$

$T \to \cdot T*F$

$T \to \cdot F$

$F \to \cdot id$

$F \to \cdot (E)$

这个闭包中,定点右边会可能出现 $E$、$T$、$F$、$id$ 和 $($ 这五个符号,因此对应的 $\text{GOTO}$ 也只存在五个,其内容分别为(只列出内核项):

$\text{GOTO}(I, E) = [ E' \to E \cdot,\ E \to E \cdot +T ] $

$\text{GOTO}(I, T) = [ E \to T \cdot,\ T \to T \cdot *F ] $

$\text{GOTO}(I, F) = [ T \to F \cdot] $

$\text{GOTO}(I, id) = [ F \to id \cdot] $

$\text{GOTO}(I, () = [ F \to ( \cdot E)] $

如果计算出算式文法的完整项集,那么其自动机如下图所示,其中阴影部分表示闭包:

图 1 算式文法的 LR(0) 自动机,图片来自编译原理

构造 LR(0) 自动机的具体实现可以参见这里

三、构造 LR 语法分析表

当然,在实际使用中,肯定要将自动机转换为其它易于处理的的数据结构,就是 LR 语法分析表。

LR 语法分析器一般都会包含两个栈:状态栈和符号栈。状态栈就代表了已归约的非终结符,与余下的输入一起表示了如下的最右句型(状态栈右侧为栈顶)。

$$X_1X_2 \cdots X_ma_ia_{i+1} \cdots a_n$$

本来根据状态栈就足够复原出相应的符号了,但在实际使用中,符号一般都会附加一些额外数据,因此需要一个符号栈来维护这些额外数据。

然后,就需要两个表格 $\text{ACTION}$ 和 $\text{GOTO}$。

$\text{ACTION}[i, a]$ 表示当前处于自动机的状态 $i$ 时,下一个输入是终结符 $a$($a$ 也可能是输入的结束 $\$$)需要执行的动作,其可能的值为:

  1. 移入 $j$,其中 $j$ 是一个状态。表示需要将 $j$ 移入栈中,同时将 $a$ 也移入符号栈。
  2. 归约 $A \to \beta$,其中 $k$ 是产生式的索引。表示需要将栈顶的 $\beta$ 归约为产生式头 $A$,弹出栈顶的多个状态和符号($\beta$ 长度个),再将归约后的状态和符号压入栈中。
  3. 接受,表示完成了语法分析过程。
  4. 报错,$\text{ACTION}$ 表格中一般不会特意写明。表示在输入中发现了一个错误并应当执行某个错误恢复动作,会在后面再来具体讨论。

$\text{GOTO}$ 表格则与之前的 $\text{GOTO}$ 函数一致,只是用状态来代表项集,并且只需要包含非终结符部分。它的用途是在遇到归约动作时,确认需要将哪个状态压入状态栈中。

对于 LR(0) 文法来说,可以如下构造语法分析表,假设已构造 LR(0) 的项集族 ${I_0, I_1, \cdots, I_n}$:

  1. 根据 $I_i$ 构造得到状态 $i$,状态 $i$ 的 $\text{ACTION}$ 根据以下方法决定:

    1. 如果 $A \to \alpha \cdot a \beta$ 在 $I_i$ 中,且 $\text{GOTO}(I_i, a) = I_j$,那么将 $\text{ACTION}[i, a]$ 设置为“移入 $j$”。
    2. 如果 $A \to \alpha \cdot$ 在 $I_i$中,那么对于任意非终结符 $x$(包含输入结束),将 $\text{ACTION}[i, x]$ 设置为“归约 $A \to \alpha$”
    3. 如果 $S' \to S \cdot$ 在 $I_i$ 中,那么将 $\text{ACTION}[i, \$]$ 设置为“接受”。
  2. 状态 $i$ 的 $\text{GOTO}$ 根据以下方法决定:设 $A$ 是一个非终结符,如果 $\text{GOTO}(I_i, A) = I_j$,那么 $\text{GOTO}[i, A] = j$。
  3. 规则 1 和 2 未定义的所有条目都设置为“报错”。
  4. 语法分析器的初始状态就是根据 $S' \to \cdot S$ 所在项集构造得到的状态。

上面算式文法生成的 LR(0) 语法分析表如下所示:

$$\begin{array}

{|c|cccccc|ccc|}

状态 & id & + & * & ( & ) & \$ & E & T & F \\

0 & s5 & & & s4 & & & 1 & 2 & 3 \\

1 & & s6 & & & & acc & & & \\

2 & r2 & r2 & r2/s7 & r2 & r2 & r2 & & & \\

3 & r4 & r4 & r4 & r4 & r4 & r4 & & & \\

4 & s5 & & & s4 & & & 8 & 2 & 3 \\

5 & r5 & r5 & r5 & r5 & r5 & r5 & & & \\

6 & s5 & & & s4 & & & & 9 & 3 \\

7 & s5 & & & s4 & & & & & 10 \\

8 & & s6 & & & s11& & & & \\

9 & r1 & r1 & r1/s7 & r1 & r1 & r1 & & & \\

10 & r3 & r3 & r3 & r3 & r3 & r3 & & & \\

11 & r6 & r6 & r6 & r6 & r6 & r6 & & & \\

\end{array}$$

这里使用 si 表示“移入 $i$,rj 表示按照索引为 $j$ 的产生式归约,acc 表示接受,空白表示报错。

如果注意检查前面的 LR(0) 自动机和语法分析表,可以发现状态 2 是包含 $E \to T \cdot$ 和 $T \to T \cdot * F$ 这两个项的,这两个项在 * 上对应的动作应当是 r2 和 s7 —— 同一个非终结符上可能出现两个不同的动作,无法不在查看更多输入的前提下决定使用哪个动作。这就说明上面的算式文法存在冲突动作,不是 LR(0) 文法,状态 9 也会有同样问题。

这里的移入-归约冲突,就是 LR 语法分析中可能遇到的冲突之一,另一个则是归约-归约冲突,这种情况下无法选择使用哪个产生式进行归约。为了解决冲突,最简单的办法就是向前查看更多符号。例如同样是基于 LR(0) 自动机,但利用 $\text{FOLLOW}$ 集减少冲突的 SLR 技术,或者利用向前看符号的 LALR 技术,或者是直接扩展为 LR(1) 语法分析。

如果允许向前查看一个字符,那么在到达状态 2 时,就可以发现在后一个字符是“”时,只能选择移入而不能归约,因为归约后的非终结符是 $E$,但却不存在 $X \to E * \cdots$ 这样的产生式。状态 9 也是同理,在遇到“”时只能选择移入。

使用修正后的语法分析表,就可以正确对 $(id+id)$ 进行语法分析了,其过程如下所示:

状态栈 符号栈 输入 动作
0 $(id_1+id_2)\$$ 移入到 4
0 4 $($ $id_1+id_2)\$$ 移入到 5
0 4 5 $(id_1$ $+id_2)\$$ 按照 5 $F \to id$ 归约
0 4 3 $(F$ $+id_2)\$$ 按照 4 $T \to F$ 归约
0 4 2 $(T$ $+id_2)\$$ 按照 2 $E \to T$ 归约
0 4 8 $(E$ $+id_2)\$$ 移入到 6
0 4 8 6 $(E+$ $id_2)\$$ 移入到 5
0 4 8 6 5 $(E+id_2$ $)\$$ 按照 5 $F \to id$
0 4 8 6 3 $(E+F$ $)\$$ 按照 4 $T \to F$
0 4 8 6 9 $(E+T$ $)\$$ 按照 1 $E \to E + T$
0 4 8 $(E$ $)\$$ 移入到 11
0 4 8 11 $(E)$ $\$$ 按照 6 $F \to (E)$ 归约
0 3 $F$ $\$$ 按照 4 $T \to F$ 归约
0 2 $T$ $\$$ 按照 2 $E \to T$ 归约
0 1 $E$ $\$$ 接受

有了 LR(0) 语法分析作为基础,下一章就会来介绍 LALR 语法分析。

本系列相关代码都可以在这里找到。

C# 语法分析器(二)LR(0) 语法分析的更多相关文章

  1. 语法分析器初步学习——LISP语法分析

    语法分析器初步学习——LISP语法分析 本文参考自vczh的<如何手写语法分析器>. LISP的表达式是按照前缀的形式写的,比如(1+2)*(3+4)在LISP中会写成(*(+ 1 2)( ...

  2. LR(0)语法分析

    # include <stdio.h> # include <string.h> //存储LR(0)分析表 struct node { char ch; int num; }; ...

  3. LR(1)语法分析器生成器(生成Action表和Goto表)java实现(二)

    本来这次想好好写一下博客的...结果耐心有限,又想着烂尾总比断更好些.于是还是把后续代码贴上.不过后续代码是继续贴在BNF容器里面的...可能会显得有些臃肿.但目前管不了那么多了.先贴上来吧hhh.说 ...

  4. LR(1)语法分析器生成器(生成Action表和Goto表)java实现(一)

    序言 : 在看过<自己实现编译器链接器>源码之后,最近在看<编译器设计>,但感觉伪代码还是有点太浮空.没有掌握的感觉,也因为内网几乎没有LR(1)语法分析器生成器的内容,于是我 ...

  5. 03.从0实现一个JVM语言系列之语法分析器-Parser-03月01日更新

    从0实现JVM语言之语法分析器-Parser 相较于之前有较大更新, 老朋友们可以复盘或者针对bug留言, 我会看到之后答复您! 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个 ...

  6. Swift3.0基础语法学习<二>

    对象和类: // // ViewController2.swift // SwiftBasicDemo // // Created by 思 彭 on 16/11/15. // Copyright © ...

  7. LL(1),LR(0),SLR(1),LALR(1),LR(1)对比与分析

    前言:考虑到这几种文法如果把具体内容讲下来肯定篇幅太长,而且繁多的符号对初学者肯定是极不友好的,而且我相信看这篇博客的人已经对这几个文法已经有所了解了,本篇博客的内容只是对 这几个文法做一下对比,加深 ...

  8. 开源语法分析器--ANTLR

      序言 有的时候,我还真是怀疑过上本科时候学的那些原理课究竟是不是在浪费时间.比方学完操作系统原理之后我们并不能自己动手实现一个操作系统:学完数据库原理我们也不能弄出个像样的DBMS出来:相同,学完 ...

  9. LL(1),LR(0),SLR(1),LR(1),LALR(1)的 联系与区别

    一:LR(0),SLR(1),规范LR(1),LALR(1)的关系     首先LL(1)分析法是自上而下的分析法.LR(0),LR(1),SLR(1),LALR(1)是自下而上的分析法.       ...

随机推荐

  1. 解决使用(Jenkins检出代码)git clone检出代码提示必须安装 .NET framework,Version =v4.7.2

    一.事件背景 真的是非常想使用pipeline流水线进行自动化部署打包测试. 于是,晚上下班回家后,真的是"现学现卖",开始做流水线脚本. 经过不懈努力,熬到凌晨两点多,终于把整个 ...

  2. Excelize 2.5.0 正式发布,这些新增功能值得关注

    Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准.可以使用它来读取.写入由 Microsoft Exc ...

  3. windows绕过杀软添加账户密码

    windows绕过杀软添加账户密码 起因:system权限下存在杀软无法添加账户信息 绕过方法 1.C#脚本 运行后会在目标机器上创建一个用户为 wh4am1 密码为 qqai@love 的 Admi ...

  4. 02_Django-路由配置-HTTP协议的请求和响应

    02_Django-路由配置-HTTP协议的请求和响应 视频:https://www.bilibili.com/video/BV1vK4y1o7jH 博客:https://blog.csdn.net/ ...

  5. PGCrypto 加密组件使用

    PGCrypto 插件提供了两类加密算法:单向加密和双向加密. 单向加密属于不可逆加密,无法根据密文解密出明文,适用于数据的验证,例如登录密码验证.常用的单向加密算法有 MD5.SHA.HAC 等.这 ...

  6. KingbaseES 数据库Windows环境下注册数据库服务

    关键字: KingbaseES.Java.Register.服务注册 一.安装前准备 1.1 软件环境要求 金仓数据库管理系统KingbaseES V8.0支持微软Windows 7.Windows ...

  7. git revert总结

    git revert git revert 是一种创建一次新的commit 来回退某次或某几次commit的一种方式 命令 // 创建一个新的commit,这个commit会删除(下面)commit- ...

  8. Python入门系列(十一)一篇搞定python操作MySQL数据库

    开始 安装MySQL驱动 $ python -m pip install mysql-connector-python 测试MySQL连接器 import mysql.connector 测试MySQ ...

  9. 解决swiper组件autoplay报错问题

    最近在自定义一个swiper 插件 发现引用之后不定时一直在报错 Uncaught TypeError: Cannot read properties of undefined (reading 'a ...

  10. PHP之旅---出发(php+apache+MySQL)

    @ 目录 前言 准备 php安装 Apache安装 MySQL安装 Navicat安装(附) Apache+php整合 验证Apache+php 前言 本文详细介绍php+apache+MySQL在w ...