编译原理笔记
文章目录
第一章 引论
1.1 语言处理器
编译器
阅读以某一种语言(源代码)编写的程序,并把该程序翻译成为一个等价的、用另一种语言(目标语言)编写的程序。
解释器
另一种常见的语言处理器,并不通过翻译的方式生成目标程序,直接利用用户提供的输入执行源程序中指定的操作。
预处理器:一个源程序可能被分割成多个模块,并存放于独立的文件中,把源程序聚合在一起的任务有时会由一个被成为预处理器的程序独立完成。
汇编器:对编译器产生的汇编语言程序进行处理,并生成可重定位的机器代码。
链接器/加载器:一个文件中的代码可能指向另一个文件中的位置,而链接器能够解决外部内存地址的问题。最后,加载器把所有的可执行目标文件放到内存中执行。
1.2 一个编译器的结构
编译器的两个组成部分:分析部分和综合部分。
一个编译器的各个步骤:
- 词法分析
词法分析/扫描:词法分析器读入组成源程序的字符流,并且将它们组织成为有意义的词素的序列。每个词素都被映射成如下语法单元,传送给语法分析:
<token-name, attribute-value>
分隔词素的空格会被词法分析器忽略掉。
- 语法分析
语法分析/解析:语法分析器使用由词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。
- 语义分析
语义分析器使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。它同时也收集类型信息,并把这些信息存放在语法树或者符号表中,以便在随后的中间代码生成过程中使用。
- 中间代码生成
三地址代码的中间表示形式。
-
代码优化
-
代码生成
代码生成器以源程序的中间表示形式作为输入,并把它映射到目标语言。
- 符号表管理
-
将多个步骤组合成趟
-
编译器构造工具
一个赋值语句的翻译:
第二章 一个简单的语法制导翻译器
2.7 符号表
2.7.1 为每个作用域设置一个符号表
最近嵌套规则
一个标识符x在最近声明x的作用域中,也就是说,从x出现的块开始,从内向外检查各个块时找到的第一个对x的声明。
符号表支持的三种操作:
- 创建一个新符号表
- 在当前表中加入一个新的条目(键-值对)
- 得到一个标识符的条目
2.7.2 符号表的使用
第三章 词法分析
3.1 词法分析器的作用
词法分析是编译的第一阶段。词法分析器的主要任务是读入源程序的输入字符、将它们组成词素,生成并输出一个词法单元序列,每个词法单元序列对应于一个词素。
词法分析器在编译器中负责读取源程序,因此还会完成一些识别词素之外的其他任务。
- 任务一:过滤源程序中的注释和空白
- 任务二:将编译器生成的错误信息和源程序的位置联系起来
有时,词法分析器也可以分成两个级联的处理阶段
- 扫描阶段主要负责完成一些不需要生成词法单元的简单处理,比如删除注释和将多个连续的空白字符压缩成一个字符
- 词法分析阶段是较为复杂的部分,它处理扫描阶段的输出并生成词法单元
3.1.1 词法分析及语法分析
编译的分析部分划分为词法分析和语法分析阶段的原因:
- 最重要的考虑是简化编译器的设计
- 提高编译器的效率
- 增强编译器的可移植性
3.1.2 词法单元、模式和词素
3.1.3 词法单元的属性
一个标识符(id)的属性值是一个指向符号表中该标识符对应条目的指针。
3.1.4 词法错误
"恐慌模式"恢复:从剩余的输入中不断删除字符,直到词法分析器能够在剩余输入的开头发现一个正确的词法单元为止。
其他错误恢复动作:
3.2 输入缓冲
加快源程序读入速度
3.2.1 缓冲区对
3.2.2 哨兵标记
每次对于forward指针需要检查两步:是否到达缓冲区末尾;确定读入字符是什么。(eof只能代表缓冲区末尾)
就是说,引入哨兵标记后每次只要检查forward指针的读入字符是什么即可。
3.3 词法单元的规约
正则表达式是一种用来描述词素模式的重要表示方法。
3.3.1 串和语言
字母表:一个有限的符号集合
串:某个字母表中符号的一个有穷序列
空串:长度为0的串,用ε表示
语言:某个给定字母表上一个任意的可数的串集合
3.3.2 语言上的运算
3.3.3 正则表达式
letter_表示任一字母或下划线,digit表示数位(任一单个数字)
3.3.4 正则定义
前面的d为后面的d铺垫,方便后面使用,从而达到最后的d可以用∑和前面的d表示
3.3.5 正则表达式的扩展
- 一个或多个实例:单目后缀运算符+
- 零个或一个实例:单目后缀运算符?
- 字符类:[ ]
3.4 词法单元的识别
3.4.1 状态转换图
- 圆圈或结点表示状态
- 边表示从一个状态指向另一个状态
- 边的标号包含了一个或多个符合,表示从某个状态经过某下个输入符合进入边指向的另一端状态
示例:
3.4.2 保留字和标识符的识别
我们可以使用两种方法来处理那些看起来像标识符的保留字:
- 初始化时就将各个保留字填入符号表中
- 为每个关键字建立单独的状态转换图
3.4.4 基于状态转换图的词法分析器的体系结构
示例:
3.6 有穷自动机
Lex(词法分析器生成工具)将它的输入程序变成一个词法分析器转换的核心是被称为有穷自动机的表示方法。这些自动机在本质上是与状态转换图类似的图,但有如下几点不同:
- 有穷自动机是识别器,它们只能对每个可能的输入串简单地回答“是”或“否”
- 有穷自动机分为两类:
- 不确定的有穷自动机(NFA):对其边上的标号没有任何限制。一个符号标记离开同一状态的多条边,并且空串ε也可以作为标号的边
- 确定的有穷自动机(DFA):对于每个状态及自动机输入字母表中的每个符号,有且只有一条离开该状态、以该符号为标号的边
确定的和不确定的有穷自动机能识别的语言的集合是相同的。事实上,这些语言的集合正好是能够用正则表达式描述的语言的集合。这个集合中的语言称为正则语言。
3.6.1 不确定的有穷自动机
示例:
状态0可以由a到达状态0或状态1
3.6.2 转换表
示例:
3.6.3 自动机中输入字符串的接受
只要存在某条[其标号序列为某符号串的]路径能够从开始状态到达某个接受状态,NFA就接受这个串。
路径中的ε标号将被忽略
3.6.4 确定的有穷自动机
DFA相比NFA,NFA的符号有空串ε且同一个符号可以由同个状态出发到达多个状态,DFA的符号没有空串符号ε且每个状态的每个符号的出边存在且唯一
示例:
3.7 从正则表达式到自动机
NFA对于一个输入符号可以选择不同的转换,它还可以执行输入ε上的转换,甚至可以选择是对ε或是对真实的输入符号执行转换,因此对NFA的模拟不如对DFA的模拟直接。于是,我们需要将一个NFA转换为一个识别相同语言的DFA。
3.7.1 从NFA到DFA的转换
子集构造法
将NFA转换为DFA,通过子集构造法构造转换表Dtran得到DFA:
-
Dtran各行:NFA状态集–DFA状态–DFA各个出边
-
Dstates初始为ε-closure(s0),且未标记
-
当Dstates中没用未标记的状态时,即DFA的状态都被确定,停止循环就得到了DFA:
- 将Dstates中未标记的状态通过出边达到的状态集{ε-closure[move(T,a)]}构成新的状态,如果不在Dstates中,则加入Dstates中
-
通过DFA的每个状态和其对应的出边即可画出DFA
示例:
3.7.2 NFA的模拟
3.7.3 NFA模拟的效率
算法总效率:O(k(n + m))
3.7.4 从正则表达式构造NFA
将正则表达式转化为一个NFA的McMaughton-Yamada-Thompson算法
基本规则:
归纳规则:
将正则表达式转化为NFA,先画出由表达式构成的语法分析树,最小到每个表达式不能再划分。每个最小的表达式由基本规则构造出NFA,然后四种情况(并、连接、闭包、括号)根据归纳规则合成最终表达式的NFA。
示例:
3.9 基于DFA的模式匹配器的优化
3.9.6 最小化一个DFA的状态数
我们总是希望使用的DFA的状态数量尽可能地少。
只需要改变状态名字就可以将一个自动机转换成另一个自动机,我们就说这两个自动机是同构的。
任何正则语言都有一个唯一的(不计同构)状态数目最少的DFA。
从任意一个接受相同语言的DFA出发,通过分组合并等价的状态,我们总是可以构建得到这个状态数最少的DFA。
算法:
最小化一个DFA的状态数量,进行以下步骤:
- 将DFA划分为Π,包含两组:接受状态组和非接受状态组
- 对于Π的每个组分别进行如下判断
- 该组的每个状态经过相同的输入符号(每个符号都有进行判断)都能到达当前Π的相同组,若到达不同的组,则根据到达的组是否相同划分当前分组
- 将这些更小的组形成新的划分Π,直至Π不变
- 再最后的Π中,每个组选取一个状态作为代表构建D’,其中:
- D’的开始状态是包含了D的开始状态的组的代表
- D’的接受状态是那些包含了D的接受状态的组的代表
示例:
补充:用程序实现DFA
示例1:
示例2:
用代码的位置来表示所处的状态,只能用于简单的情况,情况复杂时使用以下两种方法。注意:中括号[]的字符在程序中不要更新输入缓冲。
方法一 双层case的实现
示例1:
示例2:
当终态还能接受字符时,通过改写DFA实现:
方法二 表驱动的实现
示例:
表驱动方法的优缺点:
第四章 语法分析
4.1 引论
4.1.1 语法分析器的作用
语法分析器构造出一棵语法分析树,并把它传递给编译器的其他部分近一步处理。
4.2 上下无关文法
4.2.1 上下无关文法的正式定义
4.2.3 推导
句型:可能包含终结符号又包含非终结符号,也可能是空串
句子:不包含非终结符号的句型
上下文无关语言:由文法生成的语言
文法等价:两个文法生成相同语言
最右推导有时也称为规范推导。
4.2.4 语法分析树和推导
语法分析树和最左推导/最右推导之间存在着一种一对一的关系。每一个语法分析树都和唯一的最左推导及最右推导相关联。(1对1+1)
4.2.5 二义性
即一个没有二义性的文法对一个句子只能有一个语法分析树、一个最左推导和一个最右推导。
4.2.7 上下文无关文法和正则表达式
文法是比正则表达式表达能力更强的表示方法。
每个可以使用正则表达式描述的构造都可以使用文法来描述,但是反之不成立。
4.3 设计文法
4.3.1 词法分析和语法分析
正则表达式最适合描述诸如标识符、常量、关键字、空白等语言构造的结构。文法最适合描述嵌套结构,比如对称的括号对,匹配的begin-end,相互对应的if-then-else等。这些嵌套结构不能使用正则表达式描述。
4.3.2 消除二义性
优先级
结合性
左递归表示了左结合性,右递归表示了右结合性。
悬挂else
采用就近匹配原则,有else就会和前面没有被匹配的if进行匹配
4.3.3 左递归的消除
自顶向下语法分析方法不能处理左递归文法,因此使用将左递归转换为右递归来消除左递归
- 立即(直接)左递归
- 没有环或者ε产生式的文法G(间接左递归)
就是把每个非终结符的产生式右侧的【每个序号在这个非终结符前面的非终结符】用它(序号在前的那个非终结符)的产生式替换掉它
4.3.4 提取左公因子
提取左公因子是一种文法转换方法,它可以产生适用于预测分析技术或自顶向下分析技术的文法。
间接左因子常采用代入法:
4.4 自顶向下的语法分析
自顶向下语法分析可以被看作是输入串构造语法分析树的问题,它从语法分析树的根节点开始,按照先根次序(深度优先),创建这棵语法分析树的各个节点。自顶向下语法分析也可以被看作寻找输入串的最左推导的过程。
自顶向下分析算法:
- 回溯分析(功能强大,但通常较慢,不适合实际构造编译器)
- 预测分析(利用一个或多个输入记号来预测在多个产生式中怎么选择)【本章讨论】
- 递归下降
- LL(k)分析(向前看k个输入符号的预测,本章学习LL(1)文法)
4.4.1 递归下降的语法分析
递归下降分析
将一个非终结符的文法规则看作是识别A的一个过程的定义。
就是说对文法中的每个非终结符都有一个(使用它的产生式右侧作为规则)识别它的程序,该程序中每个终结符都对应了match()函数进行输入匹配的检查;每个非终结符都对应了调用相应非终结符的程序;产生式中的选择对应程序代码中的case或者if结构。
示例:
由于左递归会使得程序不停的递归下去,所以需要使用EBNF{ }对文法进行改写才能使用自顶向下的分析方法
当有左因子时,通过EBNF[ ]改写文法
使用EBNF后通过程序处理运算顺序
在程序中构建语法树
4.4.2 FIRST和FOLLOW
FIRST集
计算文法非终结符的First集算法:
- 将所有非终结符的First集置为空集
- 重复下面的步骤直到所有非终结符的First集不发生变化
- 对每个非终结符的产生式(不含选择,有选择就拆分)A→X1X2…Xn进行下面的操作
- Xi从X1开始,将First(Xi)-{ε}加入First(A)中。如果Xi可推出空集,继续找X(i+1);如果Xi不可以推出空集,则停止操作
- 如果X1X2…Xn都可以推出空集,则将ε加入First(A)中。
示例:
FOLLOW集
计算FOLLOW集算法:
- 开始符号的Follow集初始化为{$},其他非终结符的Follow集置为空
- 重复下面的步骤直到所有非终结符的Follow集不发生变化
- 对每个非终结符的产生式(不含选择,有选择就拆分)A→X1X2…Xn进行下面的操作
- 对每个非终结符Xi,将Xi后的串(Xi+1…Xn)的First集除去{ε}加入Xi的Follow集中
- 如果串(Xi+1…Xn)的First集中包含空集,则将A的Follow集加入Xi的Follow集中
示例:
注意:
- First集中没有$,Follow集中没有ε。
- First集是找产生式左侧非终结符的,Follow集是找产生式右侧非终结符的。
4.4.3 LL(1)文法
判断一个串是否符合某LL(1)文法,通常使用LL(1)分析方法。LL(1)分析方法需要用到LL(1)分析表,而LL(1)分析表可以通过First集和Follow集构造。
LL(1)分析表
分析表中横向为所有的非终结符,$表示串结束符;纵向为所有终结符。
简单文法的LL(1)分析表构造:
即如果A不能推出空集,则只考虑第一条;如果可以推出空集,那么观察S 经 若 干 步 推 导 后 非 终 结 符 A 可 以 出 现 在 终 结 符 a ∗ ∗ ( 包 括 经若干步推导后非终结符A可以出现在终结符a**(包括 经若干步推导后非终结符A可以出现在终结符a∗∗(包括)**的前面,则将对应的A→α填入[A,a]中。
一般文法的LL(1)分析表构造(采用First和Follow集):
注意是观察First(α)
LL(1)文法
等价于:
LL(1)文法不允许一个格子包含两条产生式(即二义性),可以在表格标注采用一条产生式来消除二义性,使得文法能用LL(1)方法进行分析。
4.4.4 非递归的预测分析
LL(1)分析
LL(1)分析过程
通常利用LL(1)分析表进行分析
- accept
栈和输入都只剩下$代表串是该文法可以接受的串。
- error
LL(1)出错:栈顶与输入不匹配;串空栈不空。
示例:
4.5 自底向上的语法分析
自底向上的分析方法
自底向上分析的四个动作:
- 移进shift
- 归约reduce
- 接受accept
- 报错error
示例:
句柄
示例:
通过分析树找句柄:
用句柄来对句型进行归约:
归约过程就是依次找出分析树中的最左子树并去除叶子节点
4.6 LR语法分析技术介绍:简单LR技术
LR(0)项
示例:
"."表示当前分析时符号在栈还是在输入串的情况
根据"."位置的不同将LR(0)项分四类:
- 归约项:".“在右侧结尾,且”."前面不是开始符号
- 接受项:".“在右侧结尾,且”."前面是开始符号
- 移进项:"."后面是终结符
- 待归约项:"."后面是非终结符
LR(0)项的有限自动机
同时,还要有.X→X.的Goto连接
示例:
子集法构造DFA:
直接得到DFA:
总结
由文法得到NFA的步骤:
- 改写文法,得出所有的LR(0)项
- NFA的初始状态为S’→.S
- 对于每个状态,判断"."后面的符号
- 如果是终结符,则接受该终结符指向下一状态
- 如果是非终结符S,则通过ε连接分别到达S→.α的所有状态,另外还要通过S的Goto连接到"."在S后的状态
- 所有状态都不能再指向新状态时,即得到NFA
NFA没用结束状态,由分析程序决定结束
由文法得到DFA的两种方法:
Ⅰ.构造NFA,由子集构造法得到DFA
Ⅱ.不通过NFA,直接得到DFA:
- 改写文法,得出所有的LR(0)项
- DFA的初始状态为S’→.S
- 对于DFA中每个状态,若"."后面是非终结符S,那么需要将S→.α的项都加进该状态
- 通过接受"."后面的符号,到达新的状态,立马对新状态用第三步
- 所有状态都不能再指向新状态时,即得到DFA
注意DFA中的状态需要指明包含的NFA状态(LR(0)项)
LR(0)分析算法
LR(0)文法不允许某个状态中同时包含移进项和归约项(移进-归约冲突),也不允许包含两个归约项(归约-归约冲突)。移进-移进并不会冲突,因为会先移进再根据移进的字符判断转向下一个状态。
示例:
总结
用LR(0)方法分析一个串是否是某文法可识别的串,步骤为:
- 改写文法,得出所有LR(0)项,构造DFA
- 通过DFA 构造分析表
- State:DFA中的状态
- Action:DFA状态对应的动作(看其包含的LR(0)项)
- Rule:若动作为归约,则写出对应的产生式
- Input:包含所有的终结符
- Goto:包含产生式右侧所有的非终结符
- 每行中一个状态对应一个动作
- 若动作是移进,则[State, Input]和[State, Goto]填入接受该字符后进入的状态(由DFA得出)
- 若动作是归约,则[State, Rule]唯一对应了一条产生式
- 建表进行LR(0)分析
- Steps:步骤
- Stack:栈,初始化为$和初始状态
- Input:输入串和$
- Action:栈顶状态对应的动作
- 通过分析表进行LR(0)分析
- 每一步,根据栈顶状态得到动作
- 若动作是移进(Goto连接也是),则在下一步将输入串最左侧的符号移入栈顶,并通过分析表得到移入后的状态,放入栈顶
- 若动作是归约,则在下一步将归约串对应的符号和状态在栈顶都出栈,将产生式左侧的非终结符移入栈顶,并通过分析表得到移入后的状态,放入栈顶
- 若在某一步移入的串与栈顶状态不匹配,不能得到新的栈顶状态,则报错
- 若栈为 和 初 始 状 态 + 开 始 符 号 , 输 入 串 为 空 串 和 和初始状态+开始符号,输入串为空串和 和初始状态+开始符号,输入串为空串和,则代表该串为该文法可以识别的串
- 每一步,根据栈顶状态得到动作
SLR(1)分析算法
SLR(1)文法不允许某个状态中同时存在满足条件的移进项和归约项,也不允许同时存在满足条件的两个归约项。
示例:
总结
用LR(0)方法分析一个串是否是某文法可识别的串,步骤为:
- 改写文法,得出所有LR(0)项,构造DFA
- 通过DFA 构造分析表
- State:DFA中的状态
- Input:包含所有的终结符和$
- Goto:包含产生式右侧所有的非终结符
- 每个状态都根据DFA,对其所有LR(0)项进行判断:
- 移进项在对应终结符的格子中填入s和到达的状态
- 归约项在对应Follow集的格子中填入r(产生式),若产生式是开始符合推出,则填入accept
- Goto连接直接填入转换的状态即可
- 建表进行SLR(1)分析
- Steps:步骤
- Stack:栈,初始化为$和初始状态
- Input:输入串和$
- Action:栈顶状态对应的动作
- 每一步,根据栈顶状态和输入串第一个符号得到动作
- 若动作是移进(Goto连接也是),则在下一步将输入串最左侧的符号移入栈顶,并通过分析表得到移入后的状态,放入栈顶
- 若动作是归约,则在下一步将归约串对应的符号和状态在栈顶都出栈,将产生式左侧的非终结符移入栈顶,并通过分析表得到移入后的状态,放入栈顶
- 若在某一步移入的串与栈顶状态不匹配,不能得到新的栈顶状态,则报错
- 若栈为 和 初 始 状 态 + 开 始 符 号 + 某 状 态 , 输 入 串 为 空 串 和 和初始状态+开始符号+某状态,输入串为空串和 和初始状态+开始符号+某状态,输入串为空串和,若在分析表中该状态和$对应的格子为accept,则代表该串为该文法可以识别的串
练习:
第五章 语法制导的翻译
5.1 语法制导定义
语法制导翻译
语法制导定义SDD
示例:
语法制导翻译方案SDT
SDD与SDT
综合属性和继承属性
S属性的SDD
注释语法分析树
示例:
5.2 SDD的求值顺序
依赖图
通常综合属性在文法符合右边,继承属性在文法符合左边,操作对应于虚节点
示例:
拓扑排序
示例:
属性求值顺序
S属性的SDD
L属性的SDD
注:不形成环路
示例:
5.3 语法制导翻译的应用
- 抽象语法树的构造
- 类型的结构
5.4 语法制导的翻译方案
后缀翻译方案
示例:
产生式内部带有语义动作的SDT
插入动作的语法分析树
将L-SDD转换为SDT
示例:
L属性定义的SDT实现
在递归下降语法分析过程中进行翻译
示例:
第六章 中间代码生成
6.1 语法树的变体
6.2 三地址代码
三地址代码中不能有双目以上的运算,也不允许出现组合运算(每条只能有一个运算)
常见三地址指令形式
示例:
三地址代码是中间代码的一种抽象形式,具体实现包括四元式、 三元式和间接三元式。
四元式
示例:
三元式
间接三元式
6.3 类型和声明
类型表达式
示例:
类型等价
类型的声明
局部变量名的存储布局
声明的序列
6.4 表达式的翻译
表达式中的运算
"||"是连接的作用
示例:
6.5 类型检查
类型检查规则
类型转换
拓宽函数的适应
6.6 控制流
布尔表达式
短路代码
控制流语句
控制流语句的代码布局方案
布尔表达式的控制流翻译
示例: