🌈 个人主页:十二月的猫-CSDN博客
🔥 系列专栏: 🏀编译原理_十二月的猫的博客-CSDN博客💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光
目录
1. 前言
为什么打算开始这一系列的文章——编译原理🎄🎄
其实本学期开始就一直想持续更新,陆陆续续主要更新了实验部分。
正好趁着快要考试,便和大家一起花费几天的时间回顾编译原理的知识点。
目前,十二月猫的回顾计划如下🔞🔞:
祝大家都能取得好成绩呀~~🥰🥰
参考书籍:
英文名:Compilers: Principles,Techniques,and Tools (龙书)🦖
作者:Alfred V.Aho,Ravi Sethi,Jeffrey D.Ullman
1.本课程介绍编译器构造的一般原理和基本实现方法,主要介绍编译器的各个阶段:词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成。
2.本课程在介绍命令式程序设计语言实现技术的同时,强调一些相关的理论知识,如形式语言和自动机理论、语法制导的定义和属性文法、类型论等。
3.本课程强调形式化描述技术,并以语法制导定义作为翻译的主要描述工具。
4.本课程强调对编译原理和技术在宏观上的理解,而不把读者的注意力分散到一些枝节的算法上,如计算开始符号集合和后继符号集合的算法,回填技术等。作为原理性的教材,本书介绍基本的理论和方法,而不偏向于某种源语言或目标机器。
2. 语法分析
2.1 语法分析前言
通过前一篇的学习,我们知道词法分析器会将输出结果Token交给语法分析器处理:
这部分在考试中最重要并且比较难,首先需要大致了解一以下概念:
- 文法:文法用于描述编程语言的句法结构和语法规则,也就是一大堆产生式。程序设计语言通常使用的叫做上下文无关文法(CFG)。
- 语法:语法是编程语言中定义的语法规则,用于规定程序的结构和正确组织方式。文法就是语法更加规范化统一化的表示,语法较于文法更加抽象。语法分析器根据文法规则进行词法分析和语法分析,检查源代码的语法正确性,并构建语法树表示程序的结构。
- 语法分析:语法分析就是对程序进行上下文无关部分的检查,看看程序是否符合文法所定义的句子结构。语法分析输出结果就是语法解析树。
- 语法解析树:对于前面的Token流,根据文法可以解析出来一个具有层次关系的结构,这个结构就被称为解析树。
- 建构语法解析树的方式:自上而下和自下而上方法
可以通过 【编译原理】编译原理知识点汇总·概论与文法-CSDN博客 来复习文法~~
前面我们讲过处理产生式的两个手段——推导 和 规约。
- 推导对应自上向下方法
- 规约对应自下向上方法
采用自上而下方法的分析技术主要有两种:
- 递归下降法
- LL(1)
自下而上方法的分析技术有:
- LR(0)
- LR(1)
- SLR(1)
- LALR(1)
词法分析重点内容:
两点(NFA、DFA),三线(RE--NFA、NFA--DFA、DFA优化),做词法
语法分析重点内容:
两上(递归下降法、LL(1))、四下(LR(0)、LR(1)、SLR(1)、LALR(1)),有要求
2.2 文法要求
语法分析这里提供了6种语法分析算法,但是这些算法对输入的文法是有要求的(很没有规则的一些文法是无法识别的,只能识别规则感很强的文法),不同算法要求的严格度不相同。
本部分的重点内容如下:
- 不同语法分析算法对文法的要求是什么?
- 如何转变文法满足分析算法要求?
2.2.1 上下文无关文法
上下文无关文法是所有语法分析算法最基本的要求
上下文无关文法(Context-Free Grammar,CFG)是一种形式语言描述工具,用于描述上下文无关语言的语法结构。它是计算机科学中的一种形式化的表示方法,广泛应用于编译器设计、自然语言处理和人工智能等领域。上下文无关文法由四个元素组成:
-
一个非终结符集合
-
一个终结符集合
-
一个产生式规则集合
-
一个起始符号。
其中,非终结符表示语法结构的符号,终结符表示语言中的实际词汇,产生式规则定义了语法结构的生成规则,起始符号表示语法结构的起点。
上下文无关文法的生成过程是通过不断地应用产生式规则来替换非终结符,直到最终得到一个只包含终结符的字符串,即语法结构的句子。这个过程可以看作是一个推导过程,其中每一步应用的产生式规则被称为推导步骤。
上下文无关文法可以表示一类语言,称为上下文无关语言。这类语言在形式上具有简单的结构,不依赖于上下文信息,因此被广泛应用于描述自然语言的语法结构和编程语言的语法规则。常见的编程语言如C、Java和Python都可以用上下文无关文法来描述其语法结构。
推导是指将非终结符替换为它的某个产生式的体一次替换,连续多次替换也称推导。语法分析树就是推导过程的图形表示。推导和解析树是多对一的,给定文法和某个句子,可能有多个推导对应一棵解析树,例如:
解析树无法捕捉推导所采用产生式的先后次序,但这不是问题,反而是优点,因为我们的最终目标是确定程序的结构,而推导过程本身并不是语法分析的最终目标。
根据推导的顺序可以将推导分为最左推导和最右推导:
- 最左推导
- 最右推导
二者和解析树都是一对一的,其中最右推导及其对应的最左规约也被称为规范推导和规范规约。
2.2.2 二义性
通常要求程序设计语言的文法是无二义性的,否则就会导致一个程序有多种“正确”的解释。即使文法看似允许二义性(以方便文法或语法分析器的设计),但仍需要在文法之外加以说明,来剔除不要的语法分析树。
二义性有两个主要的来源:
- 算术表达式(算符结合性二义,算符优先级二义)
- 悬空else(dangling else,else与哪个then匹配二义)
消除二义性的两个主要方法:
- 改写原文法(rewriting)
- 引入消除二义性的规则(disambiguating rule)
2.2.2.1 定义
若文法G对同一句子产生不止一棵分析树,则称G是二义的。
例3.7 句子id+id*id和id+id+id可能的分析树E→E+E | E*E |(E)| -E | id
深度越深,越远离开始节点,优先级越高。
非终结符在终结符(如+)的左边是左结合,右边是右结合。
由此可见优先级和结合性都会导致二义性
2.2.2.2 消除二义性(改写原文法)
引入消除二义性的规则:具体是在解析器(例如Yacc上)利用特殊的语句来引入结合性和优先级,从而让编译器自动处理好这个问题。
(都自动了,有什么好学习的,没意思🫠🫠)
所以,下面只学习 改写原文法
(1)改写二义文法的关键步骤:
- 划分优先级和结合性
- 引入一个新的非终结符,增加一个子结构并提高一级优先级(优先级的判断);
- 递归非终结符在终结符左边,运算具有左结合性,否则具有右结合性。
- 原数值都要保留(id、括号内的数等等),数本身优先级最高
(2)例子
例3.10 改写二义文法
E→E+E | E*E |(E)| -E | id
- 优先级从低到高:
[+]
;[*]
;[( ), -, id]
- 结合性:
左结合[+, *]
右结合[-]
无结合[id]
- 非终结符与运算:
E:+
(E产生式,左递归)T:*
(T产生式,左递归)F:-,( ),id
(F产生式,右递归)
改写后答案为:
E → E + T | T
T → T * F | F
F → (E) | -F | id
2.2.3 左递归
2.2.3.1 定义
左递归是指在产生式规则中存在直接或间接地将同一非终结符作为产生式右侧的第一个符号的情况。例如,对于产生式 A→Aα,其中 A是非终结符,α 是由终结符和非终结符组成的符号串,就存在左递归。
左递归可能导致语法分析过程中的无限循环或者无法终止,从而使分析器陷入死循环。这是因为在尝试匹配左递归产生式时,分析器会不断地展开同一非终结符,而无法向后推导到其他的产生式规则。
2.2.3.2 方法
为了消除左递归,可以采取以下步骤:
-
检测左递归:对于每个非终结符 A,检查其产生式规则中是否存在以 A 开头的产生式右侧。如果存在,则存在左递归。
-
消除直接左递归:对于存在直接左递归的非终结符 A,可以通过引入新的非终结符来消除左递归。具体步骤如下:
- 将所有以 A 开头的产生式规则拆分为两组:一组是直接左递归的规则,另一组是非左递归的规则。
- 对于直接左递归的规则,创建一个新的非终结符 A′,将这些规则中的左递归部分替换为 A′。同时,添加一个新的产生式规则 A′→βA′,其中 β 是直接左递归规则中的非左递归部分。
- 对于非左递归的规则,将其末尾添加新的产生式规则 A′→γ,其中 γ是原始规则的右侧部分。
-
消除间接左递归:如果存在间接左递归,即存在形如 A→Bα 和 B→Aβ 的产生式规则,可以通过以下步骤消除间接左递归:
- 为每个非终结符 A 创建一个新的非终结符 A′。
- 对于每个存在间接左递归的规则 A→Bα,将其替换为 A→A′α。
- 对于每个存在间接左递归的规则 B→Aβ,将其替换为 A′→Aβ。
以下为一个简单的消除直接左递归的例子:
消除直接左递归:1、创建新的非终结符;2、将左递归转为右递归;
以下为一个简单的消除间接左递归的例子:
消除间接左递归:1、将间接左递归变为直接左递归;2、消除直接左递归
2.3 自顶向下语法分析
自顶向下的语法分析技术基本思想就是沿着根到叶的方向构造解析树,直到构造出符合要求的语法解析树,否则报错。如下图所示,自顶向下的分析算法大致有如下几种:
2.3.1 回溯算法
回溯算法的核心思想是通过递归地尝试不同的产生式规则来匹配输入字符串。算法从文法的起始符号开始,根据当前输入符号和产生式规则,选择一个规则进行推导。如果选择的规则无法匹配当前输入符号,算法将回溯到上一个选择点,尝试其他的规则。这个过程会一直进行,直到找到一个匹配的规则或者所有的选择点都被尝试完。
栈:目前的状态
输入:剩下要匹配的数据
操作:对状态的操作/匹配
回溯算法有两个主要缺点:
- 对某些文法,算法会陷入无限循环(或递归)-> 需要消除左递归
- 对于简单文法尚可应付,对于复杂文法,往往因频繁回溯,耗时过多,从而不堪用;特别地,若输入有语法错误,最坏时间复杂度是指数级的 -> 引入预测分析算法
2.3.2 预测分析技术与LL(1)文法
思考回溯算法的问题——随意选择文法产生式作为操作。
想要不是随便选择,而是有目的性的选择就需要 考察后面输入的值是什么。
这也就是为什么这类算法叫做预测分析(考察下一位是什么,然后预测哪个产生式可能性高)~~
基本思想:采用深度优先策略构建解析树,每次为非终结符号选择适当的产生式,以避免回溯。具体实现上通过向前查看输入中的一个或多个尚未被匹配的终结符来选定产生式。
对于非左递归文法,因不再回溯所以速度更快;但是仍存在无法处理的文法(例如左递归文法)。
考虑通过向前查看一个输入符号就能用预测分析算法解析的文法:人们将这类文法定义为LL(1)文法。
一个文法是LL(1)文法的充要条件是L:每个非终结符A的两个不同产生式A→α,A→β满足SELECT(A→α)∩SELECT(A→β)=∅。
其中第一个“L”表示从左向右扫描输入,第二个“L”表示最左推导,“1”表示只需要看一个输入符号。这个命名规则在后面同样适用。
要进行这样的预测就需要语法分析表,构建语法分析表的过程需要FIRST和FOLLOW两个函数的辅助,二者都可以辅助选择合适的生成式:
- FIRST(α):可以出现在α导出的句型的开头的终结符的集合;如果α⇒ϵ∗,那么ϵ也包含在FIRST(α)中。
- FOLLOW(α):可能在某些句型中紧跟在α右边的终结符的集合。
FOLLOW(α):是针对于非终结符而言的,这意味着α只能是非终结符。
FIRST(α):是针对非终结符和终结符而言的,这意味着α可以是终结符也可以是非终结符。
小提示:后面的很多算法求解过程都会用到 不动点思想(While直到集合没变化停止)🤗!
2.3.2.1 FIRST集求法
规则如下:
- 如果X是一个终结符,那么FIRST(X)=X
- 如果X是一个非终结符,且X→Y1Y2...Yk∈P(k≥1),那么如果对于某个i,a在FIRST(Yi)中且ε在所有的FIRST(Y1),FIRST(Y2)...FIRST(Yi−1)中,那么把a加入FIRST(X)中
- 如果X→ε,那么把ε放入FIRST(X)中
求串的first:
-
向FIRST(X1X2...Xn)中加入FIRST(X1)中的所有非ε符号
-
如果ε在FIRST(X1)中,那么把FIRST(X2)中所有非ε符号加入FIRST(X1X2...Xn)......以此类推
-
最后,如果对于所有的i,ε在FIRST(Xi)中,那么把ε加入FIRST(X1X2...Xn)
上面的代码希望大家好好体会一下,非常有助于理解!!😊😊
2.3.2.2 求FOLLOW集
不断应用以下规则,知道没有新的终结符可以加到任何FOLLOW集合。
- 将$放入FOLLOW(S)中,其中S是开始符号,$是输入右端的结束标记
- 如果存在一个产生式A→αBβ,那么FIRST(β)中所有非ε符号都在FOLLOW(B)中
- 如果存在一个产生式A→αB,或存在A→αBβ且FIRST(β)包含ε,那么FOLLOW(A)中的所有元素都在FOLLOW(B)中这里说明了一种FOLLOW(B)对于FOLLOW(A)的依赖关系
求FOLLOW的过程是不断更新的过程,FOLLOW集直接有依赖关系,如果后面一个FOLLOW集更新了,那么所有依赖他的FOLLOW集都需要更新。可以在他们之间建一条边,说明这种依赖关系。
上面的代码希望大家好好体会一下,非常有助于理解!!😊😊
2.3.2.3 求SELECT集
SELECT集针对的是产生式
FOLLOW集和FIRST集针对的是符号(终结符、非终结符)
SELECT(A→α):要选择A→α的预测符集合
对于一个产生式A→α,求SELECT(A→α)
- 若ε∉FIRST(α),那么SELECT(A→α)=FIRST(α)
- 若ε∈FIRST(α),那么SELECT(A→α)=FIRST(α)∪FOLLOW(A)
2.3.2.4 LL(1)文法的等价变换
具体方法上面文章已有详细介绍 2.2部分
2.4 自底向上的语法分析技术(LR分析法)
2.4.1 LR分析法概述
- 上下文无关文法的LR分析法
- LR:自左至右扫描,最右推导的逆过程(也就是最左归约)
2.4.2 LR分析法
-
任务:在归约的过程中,一方面记住移入和归约的整个符号串,另一方面通过产生式推测未来可能碰到的输入符号
-
优缺点:
- 优点:文法范围广,识别能力强,可以识别出错位置
- 缺点:工作量大,需要构造这种分析程序的产生器
-
产生器作用:
- 应用产生器产生一大类上下文无关文法的LR分析程序
- 对二义性文法或难分析的特殊方法,施加一些限制使之能用LR分析法
- LR分析器(LR分析法的核心):
包括两部分:总控程序,分析表
总控程序:控制程序的运行,查分析表的内存做简单动作
产生器的任务就是产生分析表
-
分析表(LR分析器的核心):
- 一个文法的LR分析器对应四种不同分析表,所以分析表都恰好识别产生的所有语句
- LR(0),SLR,LR(1),LALR四种分析表
简单来说:
- LR分析法 依赖于 LR分析器。
- LR分析器 依赖于 LR分析表。
- LR分析表的不同就能够制造出不同的LR分析器,也就有不同的LR分析法下面的分支(例如:LR(0)、LR(1)、SLR、LALR分析法)。
因此,学习方向变为:
- 总控程序
- 不同的LR分析表
2.4.3 总控程序
总控程序就是:在已经有LR分析表的时候,程序如何判断一个句子是否符合语法要求。
一句话:怎么用LR分析表进行语法分析
LR文法分析的过程要一句LR分析表进行。还需要维护一个状态栈和符号栈。
- 对于action表中的某一列a的sn,表示将符号a、状态n入栈
- 对于action表中的rn表示使用第n个产生式进行规约。
分析过程(可能比较抽象,但是思路不难)
注意,每次都是用状态栈的栈顶状态与剩余输入的第一个在action表中进行转移。
废话不多说,直接看下面例子就明白了~~~
例1:
解答:
1、初始状态:状态为0,符号栈为#
2、输入i,按照分析表进行状态转化s5,以及符号入栈i
(最下面一条产生式没截全)
3、输入*,按照分析表进行状态转化r6,是第六条产生式规约,因此*不做输入。将i弹出,同时将状态5弹出,并放入符号F。
4、F入栈,0状态遇到F非终结符,入栈状态3
2.4.4 不同的LR分析表
通过上面的算法,想必大家已经熟悉LR分析器中的——总控程序,也就是大家已经可以自如地使用LR分析表完成LR分析了🥰🥰~~
下面,我们就来进入LR分析器中的——LR分析表
2.4.4.1 构造LR(0)分析表
LR(0)项目
右部某位置标有圆点的产生式称为相应文法的一个LR(0)项目。例如
同属于一个产生式的项目,如果圆点的位置只相差1,那么后者是前者的后继项目。例如A→a⋅Xβ的后继项目是A→aX⋅β
后继项目
原项目M+输入符号a = 原项目M的a符号后继项目M‘
例如原项目如下:
输入符号为b,则后继项目为:
前看符号
前看符号就是项目中圆点后面所有符号的集合
例如原项目如下:
前看符号为:
S、B、a、b
等价闭包
当一个项目的圆点后面是非终结符的时候,那么他就存在等价项目,例如S→v⋅I;与I→⋅I,i就是一个等价项目。因为第一个项目的圆点后面是I,所以I作为左部的产生式中,所有以圆点开头的都是他的等价项目。所有等价项目构成一个等价闭包
构造LR(0)自动机
如图所示,从初始状态开始,对于每个项目,都求一下后继项目,然后连一条边到那个项目的等价闭包。最后可以构建出如上图的自动机。
构造LR(0)自动机 过程(必记!!😘😘):
- 求出原项目的前看符号集
- 一一选出所有前看符号集中的符号,作为输入符号a
- 将输入符号a放入原项目,得到它的后继项目,并将两个项目集相连
- 求出后继项目的等价闭包
构建分析表
由上面的自动机可以构建出分析表。方法就是行上列出所有状态号,列上,终结符放入action中,非终结符放入GOTO中。可以构建出如下的分析表:
冲突
LR(0)文法是存在冲突的:
- 移入--归约冲突:某一产生式的右部是另一产生式的前缀
- 归约--归约冲突:不同产生式有相同的右部 或者 产生式的右部是另一产生式的后缀
温馨提示(必看!!):
- 冲突出现的根本原因就是归约和移入本身的判断并不准确,根据一个前看符号就选定产生式作归约着太不准确了!!!所以会出现冲突。
- 因此后面的分析表改进出发点都是想办法让规约选定产生式更加准确,也就是给他提供更多的信息
2.4.4.2 构造SLR分析表
SLR分析表就是对LR(0)分析表的一个改进
面对上面LR(0)中的冲突,我们可以进行改进得到SLR文法。方法就是在构建分析表时,根据产生式左部的follow集进行归约。
反省一下之前LR(0)文法时,如果一个状态可以归约了,那么不管他剩余输入的第一个字符是什么,都进行规约。
在SLR文法中,根据剩余输入的第一个,采取不同的规约动作。
如上图,I2I中,存在规约/规约冲突,观察B的follow集,只有d,所以在ACTION中,当待输入符号的第一个是d的时候,用第四个产生式进行归约,同理,当剩余输入的第一个是b和$的时候,采用第二个产生式进行输入。
限制:要求一个项目闭包中,归约项目的左部的follow集与移进项目的剩余输入的首字符不相同。
2.4.4.3 构造LR(1)分析表
在SLR分析的最后,介绍了SLR依然可能存在语法冲突。为什么呢?
原因:SLR只是简单地考察下一个输入符号b是否属于与归约项目A→α相关联的FOLLOW(A),但b∈FOLLOW(A)只是归约α的一个必要条件,而非充分条件
我的理解:如果输入下一个字符是b,我们采用了归约操作,那么就一定可以说明b属于A的FOLLOW集。但是我们不能说:如果b属于A的FOLLOW集,那么就一定可以对A采用归约操作。但是在SLR中,我们就是根据follow+一个前看符号来确定产生式的规约,这并不准确。不好理解的小伙伴可以参考下面的具体实例:
从上图右边生成树可以我们得知,此时 L=R 中的R后面跟着的终结符只能是$,不可能是=,但是R的FOLLOW中却包含了=。又比如说下一行中的 * R,R下一个终结符只可以是=,不会是 $ 。所以只凭FOLLOW集合判断是否采用归约是不合适。【如果使用FOLLOW 相当于归约的条件放宽了】所以,引入了LR(1)分析,用来解决这种问题。
LR(1)分析表是在LR(0)基础上加上展望符构成的
因此,我们只要来研究展望符如何生成就可以啦😇😇~~
展望符的生成
LR(1)项目定义:将一般形式为 [A→α·β, a]的项称为 LR(1) 项,其中A→αβ 是一个产生式,a 是一个终结符(这里将$视为一个特殊的终结符)它表示在当前状态下,A后面必须紧跟的终结符,称为该项的展望符(lookahead)。
- LR(1) 中的1指的是项的第二个分量的长度,也就是:往后多看一个字符
- 在形如[A→α·β, a]且β ≠ ε的项中,展望符a没有任何作用(β中可能含有其他的终结符)
- 但是一个形如[A→α·, a]的项在只有在下一个输入符号等于a时才可以按照A→α 进行归约:这样的a的集合总是FOLLOW(A)的子集,而且它通常是一个真子集
一个例子
文法如下:
步骤一:分析是否需要使用增广文法,确保最终接收状态只有一个
步骤二:写出FOLLOW集(可以全写 也可以不用 部分也行)
步骤三:分析初始状态,再根据下个可能输入的符号分析出之后的状态(类似LR(0)分析)
解释:
由0)可以推导出1式【0):左边橙色框中的式子 1:右边蓝色框中的式子】,有1式中的S是非终结符,所以又可以继续推出等价项目2和3。又2式中圆点后面的L属于非终结符,继续推出4、5式【注意:4、5中的展望符是=,因为2式中L后面有终结符=】然后继续对左边橙色框中的式子进行相同算法的推导即可。
展望符求法:
- S’的展望符肯定是$,因为S'是规约到最后的符号
- S的展望符是first_s($)
- L的展望符是first_s(=R$)
- R的展望符是first_s($)
2.4.4.4 构造LALR分析表
LALR分析表是基于LR(1)分析表的改进,改进的点仅仅在于缩小分析表大小
我们称两个LR(1)项目集具有相同的心,如果除去展望符之后,这两个集合是相同的。LALR分析器就是试图将所有同心的LR(1)项目集合并为一,如果合并后构造的分析表不存在冲突(只存在“归约-归约冲突”),则称其为文法的LALR分析表。
- 同一个项目中,若两个或多个式子,只是展望符不同,其他都一样,那么是可以写在一起的,就把展望符合在一起写即可
- 如果除展望符外,两个LR(1)项目集是相同的,则称这两个LR(1)项目集是同心的
上面这三个部分可以进一步合并
3. 总结
本文到这里就结束啦~~
本系列专栏将专注于【编译原理】知识。
内容包括:知识点讲解、习题练习、重点知识带练等~~目前已完成:
【编译原理】编译原理知识点汇总·词法分析器(正则式到NFA、NFA到DFA、DFA最小化)-CSDN博客
【编译原理】词法分析器设计(山东大学实验一)_山东大学编译原理实验-CSDN博客
【编译原理】语法、语义分析器设计(山东大学实验二)_语法分析实验-实现一个简单语法分析器(自上而下方法)实验小结-CSDN博客
【编译原理】代码生成器的构建与测试(山东大学实验三)_编译原理实验语义分析代码-CSDN博客 【编译原理】一篇搞定正规式到NFA、NFA到DFA、DFA最小化-CSDN博客
【编译原理】一篇搞定语法分析器对文法的要求(上下文无法文法、消除二义性文法、消除左递归)-CSDN博客
【编译原理】一篇搞定First集、Follow集与select集(超详细)-CSDN博客
期待您的关注~~🥰🥰
猫猫陪你永远在路上💪💪
如果觉得对你有帮助,友友们可以点个赞,收个藏呀~