现代编译原理--第二章(语法分析之LR(1))

  前面已经介绍过LL(1),以及如何使用LL(1)文法。但是LL(K)文法要求在看到K个字母的情况下必须做出预测,这相比于LR(K)文法而言就逊色很多。

  LR(K)文法的定义是:从左至右分析,最右推导,超前查看K个单词。先看一个例子,来对LR文法有个大致的印象。

  以上就是使用LR文法对源码进行分析的例子。注意到在LR文法中只有三个动作:移进,规约和接受,这三个动作也是通过查表来得到的。任何时候如果都是唯一确定这三个动作中的一个,我们就能让LR文法正确的运行。为了更好的理解LR(K)文法,我们先介绍以下最简单的LR(0)文法。

  因为动作是根据表来确定,所以,表的构建依然是我们构建的重点,先来看看一个表的最终形式:

  首先要说明的是,构建这张表的时候,我们使用到了状态机,行标就代表状态。列标由两部分组成,分别是终结符,和非终结符。s代表移进,r代表规约,g代表跳转,a代表接受,他们后面跟着的数字,除了r以外,都是状态的标号,只有r后面的数字指的时规约到第几个产生式。所有空的地方都代表出现错误。可见在非终结符下只有跳转。

  为了构建这个表,我们首先构建状态机。我们从一个基本的文法开始,文法如下:

  

  我们向产生式中添加一个点,形成这种形式,称为项。这个点的位置告诉我们当前在状态是什么。点每移动一次,我们跳转一个状态。点前面的字符串表示我们已经读取的历史,点后面的字符串表示我们希望得到的。也就是这种表达方式,既可以展望未来,也可以回顾过去。上面这个起始项中,我们希望得下一次得到一个S非终结符,可以看出1和2产生式是S的等价形式,如果我们得到1和2产生式的右部,我们就相当于得到了非终结符S,所以,我们的起始状态为:

  我们称第一个产生式为核心项,其他为普通项。这个状态我们称为状态1,所有的状态都是由这个状态中每个项的点的移动得到的。例如,状态1吃掉一个终结符x时,状态1的第二个项中的点要向右移动一位。得到状态2:

                                                                                       

  当然,状态1也可以吃掉一个终结符(,得到状态3:

                            

  状态3中的第一个项就是核心项。上面就我们说的移进操作。

  如果状态1吃掉的是一个非终结符S,那么我们称状态需要跳转,起始和移进时相似的效果。那么得到如下状态:

                            

  我们再来看状态2,目前点的位置已经到了产生式的最后面,那么意味着这个产生式已经完全匹配了,那么就可以将其规约。具体操作根据r的下标,选择产生式,将栈中的产生式的右部字符串全部弹出,将产生式的左部符号压栈,然后跳转到相应的状态。这个规约还是不太好理解,那么我们对最上面那张图的最后四个规约来举例解释一下。

  首先,要说明的是,在实际的使用过程中,在栈中的内容不包含任何的符号,只有状态编号,第一张图是为了方便大家理解,所以才将符号都放入栈中。那么,在规约弹出栈的时候,我们弹出的也都是状态编号。

  那么,对于最后四个规约的第一个规约,栈顶符号以此是  +16   (8  S12  ,18  E21  )22这六个符号以及六个个状态,只有最上面的四个满足规约,此时如果用项来表示的话,可以表示为E->(S,E).也就是说在 . 之前我们已经得到一个完整的产生式的右部,可以对其规约。需要把右部所涉及到的所有符号全部弹出,但是我们实际弹出的是状态,所以,原来的16 8 12 18 21 22 弹出状态后,得到的栈为16,我们弹出的字符串规约成为了非终结符E,此时,可以将E看作是在输入队列中输入端,得到  E??,栈顶依然是16。然后将E压栈,对应的16号状态遇到E跳转到17状态,此时栈顶为16E17。然后以此往下进行。

  以上,就是对基本概念移进,规约,跳转(可以理解为移进),接受的分析,他们都是基本的动作。根据刚才的推导过程,我们可以构建一个状态机,根据状态机我们能构建一张我们需要的表。表如果我们得到的分析表中的每一项都只有一个动作,那么我们就说这是一个无二义性的LR(0)文法。但是LR(0)文法还是可能出现二义性,我们称为移进规约冲突。首先来看一张有冲突的表:

  

  很显然,+号位置有冲突。我们来分析一下冲突产生的原因。如果输入队列输入端是+号,则状态3可以将+进栈,称为移进,到状态4,也可以进行规约,规约到产生式2。那么能不能规约呢?如果规约的话,(此时+依然在输入队列中)则规约到的E会先放到输入队列输入端,然后将E压栈,接下来+号也一定会进栈。那么在栈顶前两位中就出现了E+这样的字符串,这表面+号必须要是E的follow符号集合中一个才行。通过分析,我们知道,+不可能是E的follow集合中终结符。所以,也就不存在规约这个选项。

  所以,我们在构建表的时候,可以对终结符进行follow测试,只有在这个集合中符号才能进行规约。伪代码和最后结果如下:

                                           

  注意,第二个for中,处理的是 . 已经到产生式最末尾的项。最后得到的集合R是可以放产生式A->α的非终结符,在这些非终结符下,都可以规约到A->α。

  上面这种针对LR(0)冲突的解决方式称为SLR(0)。这是一种比较简单的解决方式,但是不代表能完美解决所有冲突。因为它使用follow集合并不是一种很精确的冲突预判集合。还是有可能出现冲突。

  那么,为什么follow集合不精确呢?我们假设有一个项包含这样的字符串αF.β,我们知道,F的follow集合包含字符串β的first集合。因此我们在SLR(0)中使用的follow集合不一定能正确的预测出产生式。但是,如果使用first集合,就能精准的预测出产生式。所以,我们重新定义了项,它包含产生式,标记状态的点,超前查看符号集合,这三个部分。使用以下的算法来计算一个状态的中每项的具体内容:

                                     

  其中Closure(I),是根据状态集合I中的核心项来产生其他的一些项,可以看出,使用到了first集合。goto就是移进操作。可见,如果当项中的 . 后有字符时,它直接开始移进,如果项中的 . 后没有字符,且输入端得字符在该项的超前查看符号集合,则规约。最后一个R集合就是规约可以进行A->α规约的非终结符集合。

使用以上算法,我们得到的就是LR(1)文法,显然,它的状态机和其他的LR(0)以及SLR(0)的状态机包含的项是不一样的。这里我们给一个例子,是关于C语言的:

                                              

  可以看出,在状态机中,有很多的状态,他们除了预测符号集合不一样以外,其他都是一样的。我们可以把这些状态合并,这样可以简化LR(1)分析表的大小。我们把合并后的状态机所描述的文法称为LALR(1)(LA:Look Ahead)文法。它需要用来存储表的空间更小 ,但是它的缺点是,有可能出现 规约—规约 冲突,但是在实际过程中,这种冲突的影响很小。

  通过这几次的讨论,我们直达了,一个语法分析程序最重要的是分析表的建立 。

  对于LL(K)类文法,他们的分析表是终结符和非终结符组成的行列索引,在表中填的是产生式。对于LR(K),他们是状态和终结符,非终结符组成的索引,表中填的是各种动作。

从分析过程上来说,LL(K)文法是自顶向下分析(由第二个L决定了遇到非终结符就开始向下分析),而LR(K)文法则是自下向上分析(R决定的)。

  其实,造成LR的能力高于LL的原因是,LL必须每输入一个字符就要决定所使用的产生式,它是关注于当下的。但是LR则会一直到获得得信息足够确定一个产生式后,才去确定,这都是在项中使用 .  带来得好处(回顾过去,展望未来)。

  以下是各种文法能力大小得比较:

                                 

时间: 2024-11-03 03:05:31

现代编译原理--第二章(语法分析之LR(1))的相关文章

编译原理-第二章 一个简单的语法指导编译器-2.4 语法制导翻译

语法制导翻译: 定义:语法制导翻译是通过向一个文法的产生式附加一些规则或程序片段而得到的 功能:给定词法单元流,通过向一个文法产生式附加一些语义动 作,语法制导分析产生语法分析树,并实现翻译动作 相关概念: 属性:表示与某个程序构造相关的任意的量,因为用文法符号(终结符号或非终结符号)来表示程序构造,所以可将属性的概念从程序构造扩展到表示这些构造的文法符号上 综合属性:如果某个属性在语法分析树节点N上的值由N的子节点和N本身的属性值确定,则该属性为综合属性,其性质为只需对语法分析树进行一次自底向

编译原理-第二章 一个简单的语法指导编译器-2.3 语法定义

语法定义: 文法定义: 定义:用以描述程序设计语言语法的表示方法——“上下文无关文法”,简称“文法”,文法自然地描述了大多数程序设计语言构造地层次化语法结构 实例: 如果用变量expr来表示表达式,用变量stmt表示语句,则 相关概念: 产生式:使用箭头(→)表示"可以具有如下形式",用相关变量表示表达式和语句的构造规则产生的式子.每个生产式包括一个称为生产式头或左部的非终结符号,一个箭头,和一个称为生产式体或右部的由终结符号组成的序列. 终结符号:有时也称为词法单元,终结符号是该文法

编译原理第一章学习(习题解答)

编译原理 第一章 引论 1.1 练习 1.编译器和解释器之间的区别是什么? 首先,编译器是一个软件系统或者说是一个程序,解释器是语言处理器.其次,编译器是把程序翻译成能被计算机执行的形式并报告翻译过程中发现的源程序的错误,解释器是直接利用用户提供的输入执行源程序中指定的操作. 2.编译器相对于解释器的优点是什么?解释器相对于编译器的优点是什么? 在把用户输入映射成为输出的过程中,由一个编译器产生的机器语言目标程序通常比一个解释器快很多.然而,解释器的错误诊断效果比编译器更好,因为它是逐句翻译源程

编译原理 实验3 语法分析

语法分析 一. 实验目的 算术表达式的文法可以是(你可以根据需要适当改变): E→E+E|E-E|E*E|E/E|(E)|i 根据算符优先分析法,将表达式进行语法分析,判断一个表达式是否正确. 二. 实验环境 操作系统:window xp 编写环境:visual c++ 编写语言:c语言 三. 实验内容 程序输入/输出示例: 如参考C语言的运算符.输入如下表达式(以分号为结束)和输出结果: (1)10; 输出:正确 (2)1+2; 输出:正确 (3)(1+2)/3+4-(5+6/7); 输出:正

编译原理第二次作业 编译器任务总结

在学习了编译原理后我开始明白编译的工作原理了,也更了解编译语言的结构.明白了编译器的编写中需要注意的各项问题,更了解了编译器的编译过程为我之后的编程提供了一些必不可少的经验,还是我的改错能力有所提高.因为写编译器使我在编程发生错误后能及时了解程序在编译过程中的原理是什么,这样我就能知道我的程序是何处的问题. 而且在学习编译原理的时候,学到了一些比较难理解的东西,通过实践不断地消化书本上的理论,最终就会有一个雏形出现.而且在编写的过程时候有一些不知道如何解决的问题时,我就会和组员讨论以得出一个可行

编译原理第二次作业——词法分析心得

今次大作业是词法分析,这次的词法分析的程序编写,有遇到很多的问题,比如说如何去定义单词与数字还有符号之间的判断关系,怎么去用数组存储输入进去的字符串,还有就是判断的一些程序不懂该如何去编写. 有一些问题我是通过百度去解决的还有一些则是通过同学的帮助而去完成的,希望下一次能自己完成不需要帮忙. 而通过这次实验,我也开始明白了编译的一些基本的原理,更加的明白编译器编写的过程需要注意的一些错误,充分认识到了自己的很多不足之处,而这也为我以后再次去编译提供很多宝贵的经验,课本有很多地方都能够在实验中去借

编译原理-第一章 引论-C和Java编译系统

C语言的编译系统: 预处理:实现文件包含#include<stdio.h>:实现宏展开#define pi 3.1415926:条件编译#if.#ifdef 汇编器:对输入进行两遍扫描. 第一遍:汇编器扫描输入,将表示存储单元的所有标识符都存入符号表,并分配地址. 第二遍:汇编器再次扫描输入,把每个操作码翻译成机器语言中代表那个操作的位串,并把代表存储单元的每个标识符翻译成为符号表中为这个标识符分配的地址. 连接器:收集.组织程序所需的不同代码和数据. 静态连接器:将多个可重定位目标文件组成一

编译原理第二次试验

#include<stdio.h> #include<string.h> void Fenxi(char c,char b); void word(char a[]); void number(char a[]); int i; int s=1; main(){     char a[100];     printf("请输入源程序:");     gets(a);     printf("%s",a);     printf("\

0916编译原理第二次上机作业

#include<stdio.h> void Fenxi(a,b); int i; main(){ char a[50]; printf("请输入源程序:"); gets(a); printf("您要分析的源程序为:"); printf("%s",a); printf("\n"); for(i=0;i<50;i++) { Fenxi(a[i],a[i+1]); } printf("\n")

0916 编译原理第二次上机实验

#include<stdio.h>#include<string.h>void Fenxi(char c,char b);void word(char a[]);void number(char a[]);int i;  //定义全局变量iint s=1; //用来记录是否存在非法字符main(){    char a[50];    printf("请输入源程序:");    gets(a);    printf("您要分析的源程序为:")