OUC中国海洋大学编译原理期末复习
第一章 绪论
编译的概念
计算机中的编译就是将 高级语言 翻译长 汇编语言或机器语言 的过程
符编译器将标识符及其各种属性放到了符号表中。符号表为每一个标识符保存一个记录的数据结构,记录的域是标识符的各个属性。
编译器的几个基本阶段,及各阶段的功能
前端也通常称为分析部分,与源语言有关
后端也通常称为综合部分,与目标语言相关
-
词法分析:从左向右逐行扫描源程序的字符,识别出各个单词、确定单词的类型。将识别出的单词转换为统一的机内表示:词法记号token(token:<记号名,属性值>)
- 将正确的单词识别为记号
- 剥去注释和空白(制表符、回车符、空格等分隔符)
- 将各阶段产生的错误信息和源程序联系起来
如果源程序支持宏,则宏的预处理可以在词法分析时实现(现代编译器往往有单独的预处理)
-
语法分析:从词法分析器输出的token序列中识别出各类短语,并构造语法树(语法树syntax tree不是AST,注意区分!)
-
语义分析:包括收集标识符的属性信息、类型检查、语义一致性检查、隐式类型转换等。目前比较流行的是使用”语法制导翻译“将语法分析和语义分析有机地组织起来
-
中间代码生成:生成后缀表示(后缀表达式)、图形表示(DAG)、三地址代码(我称其为人性化的汇编)
-
代码优化*:进行等价程序变换
-
代码生成*:由中间代码生成目标代码(汇编代码或机器代码)
编译器阶段的分组:前端、后端;遍
前端
也称为分析部分。与源语言相关,包含词法分析、语法分析、语义分析、中间代码生成
后端
也称为综合部分,与目标语言相关,包含代码生成、代码优化(依赖机器)
遍
- 几个阶段的活动可能被组合起来形成一”遍”
- 每一个“遍“都会读入一个输入文件(例如源程序或源程序的中间表示),并写一个输出文件
前端的四个阶段可以组合形成一”遍“
代码优化可以进行很多”遍“
编译器、解释器区别
编译器的工作原理如下
解释器的工作原理如下
- 编译器会生成目标程序,运行时直接执行机器指令
- 解释器不会生成慕目标程序,而是将源代码直接在目标机器上运行。执行时,解释器读取一句源代码之后,先进行词法分析和语法分析,再将源代码转换为解释器能够执行的中间代码(字节码),最后,由解释器将中间代码解释为可执行的机器指令。
第二章 词法分析

相关概念
词法记号和属性
词法记号(记号)
-
也即是token,具体内容为 <记号名,属性值>,其中属性值可选
-
记号名即记号分类,是语法分析的输入符号。确定记号名没有原则性规定,取决于处理上的方便。
模式
- 描述一个词法记号的单词可能具有的形式。当词法记号是一个关键字时,它的模式就是组成这个关键字的字符序列;当词法记号是一个标识符或其他,模式是一个更加复杂的结构,可以和很多字符串匹配
想想刷字符串算法题的时候,我们往往给匹配的字符串起名为pattern,就可以类比下这个编译原理的模式,也就是我们上面说的词法记号是一个关键字;对应到正则表达式是可以的
单词
又叫词法单元,它和某个词法记号的模式匹配,是该记号的实例
总之,我们可以看作,记号代表某一大类字符串,同一个记号的字符串满足某种模式。这一大串满足某种模式的字符串的其中一个字符串就是单词。
词法记号的属性
如果有多个单词与一个模式匹配,那么词法分析器必须向编译器的后续阶段提供有关被匹配单词的附加信息。
属性值可能是某个具体的值;指向符号表中某条目的指针(属性可能是一个结构化数据,包含单词、类型、第一次出现的位置等等)
词法错误
词法分析只能发现单词内部”拼写“类错误。包括“紧急方式”的错误恢复(删除直到正确)和错误修补(最小改动策略:包括删一个、插一个、正确代替不正确、交换相邻)
词法记号的描述和识别
字母表
符号的有限集合,用符号 \(\Sigma\) 表示。例如ASCII字符集、英文字母表
串
串是字母表中符号的 一个有穷序列。串的长度是串中符号的个数(所以空串即长度为0的串,注意空格也是一个符号,所以全是空格的串不是空串)
串有(真)前缀、(真)后缀、(真)子串;具有连接(concat)、积(指数、幂,具体来说是\(s=s^1,s^2\)就是将s重复两次)等操作
语言
字母表上的一个串集。例如所有语法正确的C程序集合,所有语法正确的英语句子等。
具有Union、Concat(两个集合求concat是笛卡尔积)、指数(自己和自己求笛卡尔积,例如\(\{0,1\}^3\)就是长度为3的由0,1组成的串),闭包,正闭包(不包括空串)等运算。

正规式(重点:根据语义写正规式;根据正规式写含义)
-
是一种特殊表达式,用于表示不太复杂的语言的”含义“。通常它按照一组定义规则,由较简单的正规式构成。
-
正规式不是串,也不是符号,它是一种模式,匹配该模式的所有串组成了一个语言。这个语言也称为正规集。
有限自动机
-
有限自动机通常用转换图表示,本质是识别器的模型化表示
-
分为确定的有限自动机(DFA)和不确定的有限自动机(NFA)。两种自动机都恰好能识别正规集。其中不确定的含义是:存在这样的状态,对于某个输入符号,它存在不止一种转换
不确定的有限自动机(NFA)
是一个数学模型,包括
- 有限的状态集合 S
- 输入符号集合 \(\Sigma\)
- 转换函数\(move:S \times (\Sigma \cup \{\epsilon\}) \rarr P(s)\) (S的幂集)
- 状态 \(s_0\) 是唯一开始状态
- \(F\subseteq S\)是接受状态集合
确定的有限自动机(DFA)
包括内容与NFA相同,区别在于
- 任何状态都没有 \(\epsilon\) 转换
- 对于任何状态 s 和任何输入符号 \(a\),DFA最多有一条标记为 \(a\) 的边离开 s,而NFA可以有多条

词法分析器功能:字符流 \(\rarr\) 记号流
我的直观就是,这里好比tokenizer,我们将句子划分为单词,以便后续处理
词法分析器的实现:正规式 \(\rarr\) NFA \(\rarr\) DFA \(\rarr\) 最简DFA \(\rarr\) 语言识别器
从自然语言到正规式
做题技巧是,找准自由板块和受约束的版块。
从正规式到有限自动机
Thompson算法
input:字母表 Σ 上的正规式 r
输出:接受语言L(r)的NFA N(r)
识别”或”正规式的NFA
s | t
识别“连接”正规式的NFA
st
识别“闭包”的正规式NFA
s*
算法性质
-
状态数最多是 r 中符号和算符总数的两倍
-
只有一个开始状态和一个接受状态,接受状态没有向外的转换
-
每个状态有一个用 Σ 的符号标记指向其他节点的转换,或者最多两个指向其他节点的 ε 转换
从NFA到DFA的变换
NFA对同一输入字符可以进入多个状态,且存在 ε 转换,会引起二义性,难以用计算机模拟。所以需要转换为DFA
子集构造法
$\textbf{ ε-closure(S) } $
即ε闭包,指从状态子集S中的任一状态出发,仅通过空转换所能到达的所有NFA状态的集合
也就是说将Dstates中所有 ε 边达到的状态加进去
$\textbf{move(S,a)} $
从状态子集 S 中的任一状态出发,通过一条标号为 \(a\) 的转换边所能到达的NFA状态的集合
也就是说看看当前Dstates集合里面经过a这条边能达到哪些状态。
算法
input:一个NFA
output:一个接受同样语言的DFA
初始:ε-closure(s0)是Dstates仅有的状态,并且尚未标记:
while(Dstates有尚未标记的状态T){
标记T;
for (每个输入符号 a){
U = ε-closure(move(T,a));
if (U不在Dstates中){
把 U 作为尚未标记的状态加入Dstates;
}
Dtrans[T,a] = U
}
}
例子
(a|b)*ab
- 先计算A = ε-closure(s0) = {0,1,2,4,7}

- 计算 ε-closure(move(A,a)) = ε-closure({3,8}) = {1,2,3,4,6,7,8}

- 将 B 作为尚未加入的状态加入表中
- 计算 ε-closure(move(A,b)) = ε-closure(move({5})) =
- 将 C 作为尚未加入的状态加入表中

- .......
计算ε-closure(move(B,a))、ε-closure(move(B,b))、ε-closure(move(C,a))、ε-closure(move(C,b))直到填满表格(无新状态).......

由于D包含了原来的状态9,所以D是接受状态
这里需要注意一点,如果得到一个新集合内容为空,那么需要新增一个死状态。例子如下
![]()
DFA的化简
上述可知,自己构造法不一定得到最简的DFA。
- 理论上,每个正规集都可以由一个状态数最少的DFA识别。若不记同构,则这个DFA是唯一的。
- 如果两个自动机的状态只有名字不同,其他都相同,则我们说这两个自动机同构
死状态
指对所有输入符号都转换到该状态本身。如下:
可区别状态
从s出发,输入w,最终停留在某个接受状态;而从t出发,输入w,最终停留在非接受状态,或者反过来。
不可区别的状态
也就是说找不到任何串来区别两个状态,也就是等价的状态。
如上述DFA中,A和B是可区别的状态;A和C是不可区别的状态:A和C通过a和b到达的状态完全相同。
极小化DFA状态数
输入:DFA M
输出 DFA M'.它与M接受同样语言,且状态数最小
1. 初始划分 Π = {F,S-F}
2. 用下面过程对 Π 构造新的划分 Π_new
for(Π中的每个子集G){
把G划分为若干子集:若要使G的两个状态s和t在同一子集中,当且仅当对任意输入符号a,s和t的a转换是到 Π 的同一子集中。
在 Π_new中,用G的划分代替G
}
3. 如果 Π_new = Π,则令Π_final = Π,执行4
4. 在Π_final的每个状态子集中选一个状态代表它,这些状态就是最简DFA M‘的状态。相应要修改状态转换表。包含s0的状态子集的代表是M’的开始状态,原先属于F集合的代表是M’的接受状态
5. 去掉M‘中的死状态和不可及的状态
核心是划分,能划分的依据是不一致(存在某个move结果不一致,注意目标状态也是一个集合而非单个状态)。
结束即不可进一步划分,那么也就是说结束的条件就是一致(存在可合并的状态时,该状态集合中的所有状态都是不可区分的)
例子1
如上图
例子2
其中s4,s5,s6是接受状态
重点例题
非形式描述的语言 <-> 正规式
做这种题的诀窍就是多写几个试试看。多多少少基于一点分块的思想。然后记得有空串的写上包括空串。
由0,1组成的,首尾为0的,长度至少为2的串
字母表{0,1}上所有串的集合(包括空串)
一种比较有说服力的做法是:0,1组成的所有串为 (0|1)*,而(ε | 0)就等价于0
由0,1组成的,倒数第三个字符为0的,长度至少为3的串
这里的方法硬要说的话就是分块吧。闭包分块,固定的0分块,后面的二选一分块。标准答案说是....以000,001,010,011结尾的任意0,1串,长度大于等于3。我感觉我这样说也没问题
含有且只含3个1的,长度至少为3的,由0,1组成的任意串
注意这里的含有且只含吧,有些时候只写了”含有“,语义不充分
0,1组成且0,1个数都为偶数(包括0)的所有串,包含空串。
这个我是真很难直观看出来。 还是基于我们分块的思想,为了让这个更直观,我们将其化简为A*(BA*BA*)*,这里就比较明显了,也就是说这个B可以出现偶数次(包含0),A可以出现任意次。而A是任意成对的00/11,B是01/10,1和0个数是奇数,两个B那么0/1出现的次数也是偶数了。

(0|ε)(10)*(1|ε)
当前接受是0,接下来期望的是1;当前接受到1,接下来期望的是0。这个状态是个死循环,关键是最后停在哪里。如果我们强制绑定01或10,最后只能得到偶数串
我觉得意思就是不要盲目绑定吧。如果绑定的内容可能有多种状态时,需要分类讨论。比如说下到题
(a*ba*ba*ba*)*
正规式 -> NFA(DFA、最简DFA)
可能需要注意的是状态标号从0开始吧
略
NFA -> DFA
将上述的NFA变DFA
DFA -> 最简DFA
非形式描述的语言/正规式 <-> NFA(DFA、最简DFA),手工构造法
简单的分析或证明
第三章 语法分析

记号流 \(\rarr\) 分析树
词法分析器中描述词法规则时使用正规式,语法分析时,很多语法规则使用正规式无法描述
为什么?我的直觉是,我们进行词法分析是一维的,产生的输出也是一维的token流。而代码不仅仅具有顺序结构,还有条件、循环等等这类层级的结构,一维结构已经无法完整描述代码。所以我们需要一个新的规则能让代码保证层级结构,同时能结束不会死循环。于是树就是一个很好的数据结构,所以我们需要某种规则将token流扩展到二维的树结构。
谈到构建树或者是遍历树,自然就会联想到递归,递归需要回溯会花费时间。所以我们往往可以通过栈这个数据结构来达成非递归。
语法结构
包括函数、表达式、语句等。可以用分析书来表示
形式语言的Chomsky分类
上下文无关文法
定义
上下文无关文法(CFG,不是控制流图哈哈)G是一个四元组\((V_T,V_N,S,P)\)
- \(V_T\):指终结符集合(非空、有限,在谈论编程语言时,终结符即词法记号名)。终结符一般包括:定义的小写字母、粗体字符串、数字、标点符号和运算符号
- \(V_N\):非终结符集合(非空集合)。终结符和非终结符交集为空。非终结符一般包括定义的大写字母,S,小写字母组成的名字
- S:开始符号,是一个非终结符,其定义的终结符串集就是文法定义的语言
- P:产生式有限集合
上下文无关的直观理解是,任何情况,只要文内存在左部非终结符,就可以被替换成右边;同样只要找到符合产生式右边的串就可以归约成该非终结符。完全不依赖于句型中的其他符号。
例子
很经典的一个例子,常常用来作为例题。考虑优先级、结合性、二义性等等

推导
把产生式看成重写规则,把符号串中的非终结符用其产生式右部得串来代替
对于上述例子,我们可以按照任意顺序对单个E不断应用各个产生式,得到一个替换序列,如:
这样我们推导证明了串 -(id+id) 是表达式得一个实例
注意推导符号和产生符号分开来。推导一般有三种符号
![]()
递归
设给定文法\(G:(V_T,V_N,S,P)\),若有 A \(\implies\) α \(\implies^*\) βAγ, β 和 γ 不同时为 ε ,则称非终结符 A 时递归的
- 若存在产生式 A \(\rarr\) βAγ,则称非终结符A是直接递归的
- 若 β = ε,则A是左递归
- 若 γ = ε,则A是右递归
如果文法中至少含有一个递归的非终结符,则此文法称为递归文法。只有文法G递归定义时,L(G)中句子才是无穷的。
最左推导
最右推导
也叫规范推导
消除左递归
消除直接左递归
对于下面式子
直接套公式!!!
可拓展为:
本质上是把左递归转换成右递归
例题
间接左递归
也就是将非直接左递归转换为直接左递归后再消除直接左递归。
如果原文法G产生了回路,则需要对非终结符进行排序,用排在后面的代换排在前面的非终结符。
提取左公因子
对于
当不清楚应该用非终结符A的哪个候选式来替换它时,可以通过重写A产生式来推迟这种决定。等读入了足够多的输入,获得足够信息后再做正确的决定。
具体来说,我们也是引入新的非终结符来提取公因子。把不相同的部分记为因子
分析树
分析树是推导的图形表示。其根节点是开始符号;内部节点是非终结符,由该非终结符的这次推导所用产生式的右部各符号从左到右依次标记;叶子节点是非终结符或终结符,从左到右构成一个句型
以下是一个最左推导的分析树
可见推导的过程就是一个由上而下简历分析树的过程
练习
二义性
考虑如下例子
文法二义指的是:该文法存某个句子,有不止一个最左(最右)推导。
消除文法二义性
但是大部分语法分析器都期望文法是无二义性的,否则我们就不能为一个句子唯一选定语法分析书。
消除二义性的关键是定义优先级和结合性,方法是引入非终结符,限制每一步推导只有唯一的选择
例一
具体来说,产生式优先级从上到下依次为从低到高,左结合为左递归,右结合为右递归。相同优先级的写在同一个产生式即可。
例二
例三-消除悬空ELSE
规定else和左边最接近的还没有配对的then相配对
自上而下分析
从文法开始符号出发,自上而下,从左到右为输入串建立分析树。总是选择每个句型的最左非终结符进行试探和替换。
这种方法存在的问题有:
- 不能处理左递归
- 需要更多临时变量来保存状态
- ..
预测分析是自上而下分析技术的一个特例,通过在输入中向前看固定个数(通常是一个符号)来选择正确的产生式。预测分析不需要回溯,是一种确定的自顶向下分析方法。而LL(1)是符合预测出分析的一种方法
LL(1)文法
关键点在于不存在回溯。没有回溯第一个前提就是没有左递归,第二是每次都能选择确定正确的产生式(也就是说没有左公因子和二义性)。对此我们保证选择正确的具体条件是:对于产生式 A -> α | β
- 任意终结符a不能同时出现在FIRST(α)和FIRST(β)中。
- 如果 β 能推导出空,那么a不能同时出现在FIRST(α) 和 FOLLOW(β)中
定义
LL(1)的含义
- 第一个L:从左到右分析
- 第二个L:最左推导
- (1):每步向前搜索一个输入符号
LL(1)文法的充要条件
任何两个产生式 A-> α|β 都满足下列条件
- \(FIRST(\alpha) \cap FIRST(\beta) = \empty\)
- 若 \(\beta \implies *\epsilon\),那么\(FIRST(\alpha) \cap FOLLOW(A) = \empty\)
LL(1)的必要条件
- 没有公共左因子
- 不是二义的
- 不含左递归
求FIRST和FOLLOW集
求FIRST
1. 如果当前读取的符号是个终结符,那么直接加入FIRST集。(终结符的FIRST集只有自己)
2. 如果当前读取的符号是个非终结符
a. 把该非终结符FIRST集里除了ε以外的所有元素加入目标FIRST集
b. 如果该非终结符的FIRST集包含ε,则读取下一个符号,把它当作新的起点,回到步骤 1
c. 如果达到产生式的末尾,发现该产生式上的所有符号都能推导出ε,则将ε加入目标的FIRST集(当然如果能直接推导出空,也将ε加入FIRST集)
3. 对文法的所有产生式反复执行1和2,直到FIRST集合彻底稳定,不再新增任何元素。
求FOLLOW
1. 将 $ 加入开始符号的FOLLOW集中
2. 若有产生式 B -> αAβ,则 FIRST(β) 中 除ε以外 的一切符号都属于FOLLOW(A)
3. 若有产生式 B -> αA,或产生式 B -> αAβ 而 FIRST(β) 中有 ε ,则FOLLOW(B) 中一切符号都要放入 FOLLOW(A) 中
所以我们可以说FOLLOW集不会出现 ε
递归下降预测分析
略
非递归预测分析(LL(1)分析)
具体结构如下:
主要分为四个结构:输入缓冲区、栈、分析表、输出流
- 输入缓冲区:存在需要解析的句子。末尾必须加上一个特殊的结束符 $
- 符号栈:初始化时,需要先垫一个 $ ,然后再加入文法的起始符号、
- 控制器和预测表:根据栈顶符号和当前输入符号,去查表决定下一步动作
case study
对该文法求得FIRST和FOLLOW集后
构造预测分析表如下:
对于FIRST(α)中的每一个非空的终结符a,将产生式A -> α 无条件填入表中
如果FIRST(α)中有 ε ,那么对于该非终结符中的每一个终结符 b,b在FOLLOW(α)中 ,将产生式A -> α 无条件填入表中
对于输入id * id + id预测分析器如下:
查表展开
如果栈顶是个非终结符,当前输入是id,那么查表M[E,a]后
- 将栈顶的E弹出栈
- 将产生式右部 **逆序 ** 压入栈 中
- 在输出中记录这条产生式
匹配消除
如果栈顶是个终结符,也就是说没有任何产生式可以展开了。此时我们只检查它和当前输入符号是否长得一模一样,如果一样,直接把栈顶符号弹出,同时把输入指针向后移动一位。注意此时的输出为空。
最后如果栈顶和缓冲区都只剩下 $ 的话,说明匹配完美结束了
自下而上分析
归约和句柄的概念
最左(最右)推导的逆过程就是最右(左)归约。其中最左归约也称为规范归约。
值得注意的是:二义文法不是LR(k)的,但是移进-归约分析可以用来分析某些二义文法,只要给出解决冲突的策略
句柄
句柄是归约的一个子串。如果一个句型由最右推导得到,则句型的句柄是和某产生式右部匹配的子串,并且,把它归约成该产生式左部的非终结符代表了最右推导过程的逆过程的一步
推导的时候用的哪一块,反过来归约的时候这一块就是句柄
所以如果文法二义,那么句柄可能不唯一,例下
这里的
rm是最右推导的意思(rightmost),lf就是最左推导(leftmost)注意句柄是推导得到的结果,是一整个代换式哦
具有以下性质
- 如果文法无二义时,每个右句型都有唯一的句柄
- 对于最左归约,任意一个规范句型中,句柄的右边绝对不可能出现非终结符;反之对于最右归约,左边不存在终结符
- 对于最左归约,任意一个规范句型中,句柄的左边既可能有终结符也可能有非终结符;反之亦然
移进归约分析及冲突
归约主要包括两个问题:
- 如何确定右句型中将要归约的子串
- 如果该子串是多个产生式的右部,确定选择哪一个产生式
用栈实现移进-归约分析
和非递归预测类似,我们使用栈保存文法符号,用输入缓冲区保存要分析的串。初始化如下:
栈:$
输入:w$
- 移进:把下一个符号压入栈
- 归约:将栈顶的句柄归约为非终结符
- 接受:宣告分析成功(栈是$S,输入是$)
- 报错:发现错误,调用错误恢复例程
case study
还是输入 id * id + id
我们采用LR分析法来进行归约分析。
一般做题来说,我们先写最右推导,然后画出句柄。其逆过程就是最左归约了。
这样做往往更简单快捷一些,不然分析的时候不太能明确知道这里该移进还是归约。一旦了解了这个归约过程,那么移进-归约分析也很简单了。
重点在于第六行
是应该移进还是归约呢?如果不看推导的话,无论这里移进还是归约最后都可以化简为接受状态,这里就是典型的移进归约冲突。原因是因为语法具有二义性,同时分析器只具有局部视野。
移进-归约冲突
正如上述所示,我们没法判断当前是否为句柄。其核心在于:算符的优先级和结合性未确定;分析器视野局限。简单来说就是结束和继续之间无法选择
归约-归约冲突
核心在于:文法设计存在重叠;分析器视野局限。简单来说就是两个不同归约之间无法选择。
LR分析算法
分析表
对于文法:
E -> E+T
E -> T
T -> T*F
T -> F
F -> (E)
F -> id
LR分析法分为SLR、LR(1)、LALR。三种LR的分析算法都一样,只是构造LR分析表的方法不同,分析能力也不同
1. 初始化
栈中填入0,输入填入表达式,以$结束
2. 进行移进-归约分析
将栈顶数字作为行,输入最左端元素为列,查询表中元素
a. 如果是
sn,则将该输入元素压入栈中,并且加上数字nb. 如果是
rn,则按照对应的第n个产生式,将对应个数的符号和数字弹出,归约后将符号压入栈中,并计算新的数字。查到acc,结束;查到空白,error报错
栈中的文法符号总是形成一个活前缀。所谓活前缀,就是右句型的前缀,该前缀不超过最右句柄的右端。也就是说,栈中文法符合和剩余输入组成一个右句型,其中栈中文法符号是该右句型的前缀。由于栈顶一旦发现该句柄就会被归约,所以栈中的文法符号肯定不会超过该句柄的右端
所以栈顶的状态符号包含了确定句柄所需要的一切信息,分析表的转移函数本质上是识别活前缀的DFA的转换函数
LR对比LL
| LR(1)方法 | LL(1)方法 | |
|---|---|---|
| 对文法的显式限制 | 无二义性 | 无二义性、无左递归、无公共左因 |
| 分析表比较 | 状态 x 文法符号,分析表大 | 非终结符 x 终结符,分析表小 |
| 分析栈比较 | 状态栈,通常状态比文法符号包含更多信息 | 文法符号栈 |
| 确定句柄 | 根据栈顶状态和下一个符号便可确定句柄和归约所用产生式 | \ |
| 语法错误 | 绝不会将出错点后的符号移入分析栈 | 和LR一样 |
构造SLR(1)分析表
项目
全称为 文法的LR(0)项目。具体来说是
- 我们在右部的某个地方加点的产生式。
- 加点的目的是用来表示分析过程中的状态。
例如:
注意只能在右部加点嗷
拓广文法
简单来说就是在开始符号之前加入一个新的文法符号和产生式。拓广文法的目的是确定何时分析成功。也就是说我们使用拓广的这个第0条产生式归约时代表分析成功
构造LR(0)项目集规范族
1. 先拓广文法
2. 从项目 S' -> ·S 出发,计算闭包,得到初始状态I0
3. 对当前状态,找出所有圆点后面的不同文法符号
4. 对每个符号执行GOTO:
a. 把该符号移到圆点左边
b. 这些移动原点后直接得到的项目是核心项目
c. 计算闭包,不如非核心项目
5. 检查已有项目集
a. 如果和已有状态完全相同,就连接到已有状态
b. 如果不同,就加入为新状态
6. 对所有新状态重复上述过程,知道所有状态的出路都处理完,且不再产生新状态
关于闭包的计算,只有当 · 的后方紧挨着一个非终结符的时候,才计算这个非终结符的闭包。
有关官方版本的构造LR(0)项目集规范族算法如下
(1) 初始时,I的每个项目都加入closure(I)。 (2)如果A-> α·Bβ在closure(I)中,且 B->γ是产生式,那么如果项目B->·γ还不在closure(I)中,则把它加入。重复该规则,直到没有更多项目可加入为止。
算法
构造ACTION-GOTO表
1. 构造LR(0)项目集规范族
2. 计算所有非终结符的FOLLOW集
3. 根据终结符转换填写移进动作
如果状态 Ii 经过终结符 a 转移到状态 Ij。则在ACTION[i,a]中填入sj
4. 根据非终结符转换填写GOTO表
如果状态 Ii 经过非终结符 A 转移到状态 Ij。则在GOTO[i,A]中填入j
5. 根据归约项目填写归约动作
如果状态 Ii 中存在归约的项目 A -> α·,对FOLLOW(A)中的每个终结符 a,在ACTION[i,a]中填写 rn
6. 填写接受动作
如果状态 Ii 中存在项目 S' -> S·,则在ACTION[i,$]中填写acc
能够构造出无冲突的SLT分析表的文法就是SLR文法。SLR文法都不是二义的,但是描述能力有限,无法处理 移进-归约冲突 和 归约-归约冲突。具体例子如下,可自行尝试构建
上述构造出得DFA应该如下
![]()
这里产生冲突得地点在I2,冲突得原因是一个活前缀可能有多个有效项目。具体来说是,由于FOLLOW(E)={=,$},所以当I2遇见=得时候存在移进归约冲突。
根本原因是归约时缺乏对上下文的考虑。归约时只考虑了下一个输入符号是否属于归约项目相关联的FOLLOW(A)集合,而没考虑串α所在的右句型的上下文。对于这个例子来说,只要 = 左边的 E出现,前面一定会有 * ,此时一定会被归约成 V。因此下一个符号是 = 的情况下,分析表应该已经吧 *E 归约成 V 了,应该只执行移进操作
构造LR(1)分析表
对于上述情况,直观的解决方式是在不同的使用位置,归约时应按情况考虑FOLLOW的子集。也就是说A会要求不同的后记符号。核心解决方法时加上搜索符
搜索符
对于
搜索符是在子串αβ所在的右句型中直接跟在β后面的终结符。但 β 不为空的情况下没什么用;但当 β 为 ε 时,它决定了何时将 αβ 归约为 A
计算搜索符
对于
这个核心项目,有\([B \rarr \gamma] \in P\) 这个非核心项目,那么此时这个B的搜索符为:
注意此时b不能为空,因为a不为空
举个例子,对如下文法构造规范的LR分析表时,在构造第一个项目集时,正确的I0是:
E' -> E E -> E+T | T T -> T*F | F F -> (E) | id\[I_0 = \begin{cases} [E' \rightarrow \cdot E, \quad \$] \\ [E \rightarrow \cdot E+T, \quad + \mid \$] \\ [E \rightarrow \cdot T, \quad + \mid \$] \\ [T \rightarrow \cdot T*F, \quad + \mid * \mid \$] \\ [T \rightarrow \cdot F, \quad + \mid * \mid \$] \\ [F \rightarrow \cdot (E), \quad + \mid * \mid \$] \\ [F \rightarrow \cdot id, \quad + \mid * \mid \$] \end{cases}\]
算法
和SLR的过程基本相同,只不过需要每个项目后面加上一个搜索符。如果项目完全相同但是搜索符不同,那么就是两个状态。所以LR(1)的状态数一定 >= SLR(1)
1. 构造LR(1)项目集规范族:修改closure(I)和goto(I,X)函数,使其带上搜索符
2. 根据终结符转换填写移进动作
如果状态 Ii 经过终结符 a 转移到状态 Ij。则在ACTION[i,a]中填入sj
3. 根据非终结符转换填写GOTO表
如果状态 Ii 经过非终结符 A 转移到状态 Ij。则在GOTO[i,A]中填入j
4. 根据归约项目填写归约动作
如果状态 Ii 中存在归约的项目 A -> α,则对搜索符中的每个终结符 a,在ACTION[i,a]中填写 rn
5. 填写接受动作
如果状态 Ii 中存在项目 S' -> S·,则在ACTION[i,$]中填写acc
注意和SLR算法只有1和4有差距哦。当然绘制出的LR(1)项目集也是有差距的
构造LALR分析表
LALR通过搜索符能描述SLR无法描述的文法,但代价是增加了很多状态数。其中增加的状态数项目集和某个状态是相同的,只是搜索符不同。LALR旨在使用和SLR相同多的状态,使其分析能力介于SLR和LR之间。
注意。LALR和LR只是在归约的时候不同,LALR可能会归约更多符号,导致推迟报错。两者在移进过程中是完全相同。
考虑文法:
S' -> S
S -> BB
B -> bB
B -> a
我们构造LR(1):
其中同色的就是同心的LR(1)项目集,我们称其为同心集。合并这些状态,我们就可以得到构造LALR分析表的DFA
值得注意的是
- 同心集的合并不会产生新的 移进-归约冲突
- 同心集的合并可能会产生新的 归约-归约冲突
二义文法在LR分析中的应用
如果我们直接使用二义文法,如下
构造SLR分析表就会有冲突
此时我们规定 * 的优先级高于 +,且两者都是左结合
我们可以粗略理解成,遇到E*E优先归约;遇到E+E的话先缓缓,万一后面有*
而由于是左结合,所以前面出现E+E,又来一个+的话就先归约;前面出现E*E,后面又来一个*的话就先归约。
总之我们可以通过语义来直观理解,或者通过写几个句子来判断先归约还是移进。
其他例子:TODO
LR分析中的错误恢复
-
紧急方式错误恢复:抛弃若干个输入符号,直到合法位置。
- 举个例子,也就是说,如果在试图将子串归约为A的过程中出错,采用紧急方式错误恢复,实际上相当于假设被退栈的部分和被抛弃的输入已经成功分析出了A,从而可以继续后面的分析
-
短语级错误恢复:针对分析表中每个空白条目确定不同的最合适的恢复方法
- 实际上是为动作表中每个出错条目编写一个错误恢复子程序,分析一旦进入该出错条目,就调用该错误恢复子程序。
例题
or遇到and和or发生移进归约冲突,因为or优先级比and低,所以遇到and时移进,又因为or左结合,所以遇到or归约。
第四章 语法制导的翻译
语法制导定义(SDD)
语法分析、语义分析、中间代码生成通常通过语法制导的翻译有机得结合起来。
语法分析过程并不关心文法符号得含义。编译器得最终目的是将源程序翻译成汇编语言或机器语言形式得目标代码,这个翻译过程会涉及到语义的分析,例如需要产生代码,把信息存入符号表,显示出错信息等
值得区分的概念是语法制导定义(SDD)和语法制导翻译方案(SDT)。前者是抽象的,后者是具体的。其中SDD包括有
- 基础文法(CFG)、
- 文法符号的属性、
- 产生式的语义规则、
语法制导定义的形式
语法制导定义可以看作上下文无关文法的扩展:每个文法符号多了一组属性,每个产生式多了一组语义规则。属性一般表示一种语义信息,包括串、数值、类型等等。每个文法符号可以有多个属性。语义规则一般描述属性的计算规则。
- 每个文法符号关联一组属性
- 每个文法产生式关联一组语义规则来计算该产生式中各文法符号的属性值
通常我们将这类无意义的返回值称为副作用,相当于计算一个虚拟属性的值
对于上述终结符的属性,如 digit.lexval,我们称其为记号值
继承属性和综合属性
综合属性
文法符号E的综合属性值是通过分析树中它的子节点或它本身的属性值来计算
同时终结符的属性是综合属性。终结符的综合属性值是由词法分析器提供的词法值,SDD中没有计算终结符属性值的语义规则。
继承属性
文法符号E的继承属性值是通过分析树中的兄弟结点、父节点或它本身的属性值来计算
也就是说这些属性依赖于它们所在的上下文。
例下:
这里可以看出,id、L1的类型依赖于L,L的类型依赖于T,所以我们需要优先计算T的属性,再计算L,最后是id的属性
这种属性的依赖关系可以用一种叫做依赖图的有向图来描绘。其中对于一个文法符号,左边是继承属性,右边是综合属性。绘制的算法为:
- 为纯副作用语义引入虚拟属性(一般都是综合的)
- 分析树中如果属性b依赖于属性c,则从c的结点到b的结点有一条有向边
对于属性计算的次序,我们引入拓扑排序。按拓扑排序的次序计算属性。这种方法称为分析树方法,适合手工构造和判断。实际上有更高效的方法
S-SDD
仅仅使用综合属性的语法制导定义称为S属性定义,简称为S-SDD
S-SDD可以按照自下而上的顺序计算属性,可以和LR分析结合起来
L-SDD
简称为L属性定义。在一个产生式所关联的各属性之间,可以从左到右,但不能从右到左。当一个SDD是L-SDD,当且仅当
- 每个属性要么是综合属性
- 要么是满足下列条件的继承属性:产生式 A -> X1, X2 ... Xn, 其右部符号Xj (1<=j<=n)的继承属性仅依赖:
- A的继承属性(注意不能用父节点的综合属性!!!)
- 该产生式中Xj左边符号 X1,X2,...Xj-1 的属性
- Xj 本身的属性,但不能在依赖图中形成环
每一个S-SDD都是L-SDD
语法制导的翻译方案(SDT)
这里将语义规则进一步写成语义动作。我们可以把语义动作想想成一个文法符号,进行推导(或归约)的时候,就是该语义动作执行的时候。对于上述L-SDD的语法制导定义,我们可以写出如下语法制导翻译方案
用SDT实现S属性定义
主要用于自下而上的分析,应用LR分析。
方法
将每个语义动作都放在产生式的最后
结果应该输出一棵AST。至于AST是什么以及相关查阅 参考。值得注意的是:非终结符都是分支结点,终结符都是叶子结点。我们使用mkNode和mkLeaf函数分别制造出中间结点和叶子结点,并且返回一个指针。
具体来说,如果我们构造一棵 a+5*b 的AST如下:
综合属性可以由自下而上的分析器在分析输入的同时完成计算。实际上,我们也仅仅是用LR分析器增加一个域来保存综合属性值,归约时执行动作计算综合属性即可。
用SDT实现L属性定义
主要用于自上而下的分析,应用LL分析。
L属性定义的SDT也有可能在预测分析的同时进行属性的计算。因为不一定是仅仅只包含继承属性嘛。
S属性定义的自下而上翻译
注释分析树指结点的属性值都标注出来的分析树。计算结点属性值的过程叫做注释(加注)或修饰
对于全部都是综合属性的SDD,分析树各节点属性的计算可以自下而上得完成
使用翻译方案计算L属性定义
- 将计算某个非终结符A的继承属性的动作插入到产生式右部中紧靠在A的本次出现之前的位置上
- 将计算一个产生式左部符号的综合属性的动作放在这个产生式的最右端
例题
a是继承,b和c是综合
B.b依赖于父节点,所以b是继承属性,s是综合属性(吗?)。B.i依赖于父节点和兄弟结点,所以 i 是继承属性。又因为B依赖于C而C在B的右边,所以不是L-SDD也不是S-SDD。

对于输入 5*(4*3+2) 构建注释语法分析树
这类题永远是先写好分析树再写属性。自顶向下构建分析树也就是写一下最左推导,写一步树分裂一下。之后加入属性,左继承右综合。
第五章 类型检查
类型检查在语法分析与中间代码生成之间,和语义分析高度结合。通常我们会在设计类型系统后,在语法制导翻译方案中加入类型检查和类型转换代码。
相关概念
- 执行错误:程序运行时出现的错误。其中会被捕获的错误会引起计算立即停止;不会被捕捉的错误可能会有一段实践未引起注意。
- 良行为的程序:一个程序的运行不可能引起不会被捕获的错误,则称它为良行为的
- 安全语言:任何合法程序都是良行为的语言
- 类型化的语言:为每种运算都定义了各个运算对象和运算结果所允许类型的语言。包括显式类型化语言(Java,C++)和隐式类型化语言(类型声明可忽略,系统自动分配类型)。类型化语言不一定是类型可靠的,比如说C语言。类型的本质是变量在程序执行期间的取值范围
- 无类型语言:不限制变量的取值范围,运算可以作用于任何运算对象,结果可能位置。比如汇编语言。
- 类型化语言的类型系统:语言的组成部分由一组定型规则构成,这组规则用来给各种语言构造指派类型
- 类型检查:即根据定型规则来确定程序中各语法构造的类型。目的是拒绝那些有类型错误的程序
- 良类型的程序:能够通过类型检查的程序是良类型程序
- 类型可靠的语言:若所有良类型程序都是良行为的,则称该语言是类型可靠的。类型可靠可以保证通过了静态类型检查的程序在动态执行时不会出现不会被捕获的错误。类型可靠的语义一定是安全的语言
- 静态可靠:包括类型错误、类型化语言、良类型程序
- 动态可靠:包括执行错误、安全语言、良行为程序。
- 显式类型转换:转换由程序员写出
- 隐式类型转换:编译器自动完成转换,原则上要求不丢失信息
类型表达式
一个程序的构造类型可以用类型表达式来表示。类型表达式包括基本类型和从基本类型构造的构造类型。
其中基本类型有: interger,real, char, boolen, void` ,type_error 等等
注意type_error没有对应的类型表达式
用基本类型构造出的类型就是构造类型,其中包括:
数组构造符:array(num,T)
其中T是某个类型表达式,num是interger类型
指针构造符:pointer(T)
其中T是某个类型白哦大是
笛卡尔积 X 和 函数构造符 \(\rarr\)
例如 int X char -> bool 意思是第一个参数类型是int,第二个参数类型是char,返回结果是bool。两样都为空即 void -> void
记录构造符record
若有标识符 N1...Nn 与类型表达式 T1,T2...Tn,则record(N1:T1, N2:T2,..., Nn:Tn)是类型表达式。注意是先变量名再类型名
这几类构造类型也可以互相嵌套使用,比如array中的T可以是指针或者record。
类型系统的描述语言
类型系统是一种符号系统,来自于自然推演。包括断言、定型规则和演绎推理、类型推断。
类型系统通常包含三要素:断言、推理规则、类型表达式
断言
- \(\Gamma\) 表示类型环境,包含所有变量,变量所属的类型。相当于程序中的类型声明语句和编译器里的符号表
- \(\vdash\) 表示可证明出,或者 由此得出
- 这个断言的意思是:S的自由变量都声明在 \(\Gamma\) 中
环境断言
表示 \(\Gamma\) 是良性的环境
语法断言
这里说的是自然数在环境中。
定型断言
代表在环境 \(\Gamma\) 下,M具有类型T。其中M的自由变量都出现在 \(\Gamma\) 中。
例如\(\empty \vdash true:boolean\),\(\{x:nat\} \vdash x+1:nat\)代表x是自然数,那么x+1也是自然数
推理
推理规则是在一组已知有效断言的基础上,声称某个断言的有效性。一般形式如下
其中横线上方是前提,横线下方是结论。前提为零的规则叫做公理
环境规则
空环境是良形的。这是一个公里
语法规则
任何良形环境下,boolean是一个布尔表达式。
定型规则
两个int类型相加的表达式仍然是int类型
区分语法规则和定型规则:
![]()
语法规则在于定义什么是一个合法的类型;定型规则在于定义个计算表达式属于什么类型。一般看到结论有冒号的,就是定型表达式了
类型表达式等价
结构等价
- 两个类型表达式完全相同(当无类型名时)
- 把所有的类型名字用它们定义的类型表达式代换后,两个类型表达式完全相同(有类型名时)
名字等价
- 把每个类型名看成一个可区别的类型
- 两个类型表达式名字等价当且仅当这两个类型表达式不做名字代换就等价
结构等价也就是说,不断把宏、别名向下展开,直到全部变成基本数据类型(和指针),只要两个类型在内存里长得一样,它们就是等价的。
名字等价指显式声明为同一个标识符(名字)的变量,类型才相同。
![]()
如这道题选C
另外,对于C语言来说,
struct(包括链表)和union使用的是名字等价,typedef、指针(包括数组)使用的是结构等价。不对struct使用结构等价是因为这会导致类型等价的环。如下列:struct Node { int value; struct Node *next; // 指针引入了环 };
第六章 运行时存储空间的组织和管理
本章属于后端(综合部分)
基础概念
-
过程:是一个声明,它将过程名和过程体联系起来。在大多数语言中,有返回值的过程叫做函数。完成的程序也可以卡看作一个过程
-
过程调用:过程名出现在调用语句中,则过程在该点被调用。过程调用即执行被调用过程体。过程调用改变环境,赋值改变状态。环境把名字映射到左值(存储空间),而状态把作者映射到右值。
-
形式参数:过程定义中出现
-
实在参数:过程调用中出现,替代形式参数
-
名字的作用域:一个声明起作用的程序部分称为该声明的作用域
-
绑定:如果环境将名字x映射到存储单元s,我们就说x被绑定到s。即使一个名字在程序只声明一次,该名字在程序运行时也可能绑定到不同的存储单元。通常在递归中出现这种情况。
-
活动记录:过程的活动所需要的信息用一块连续的存储区来管理,叫做活动记录或帧(frame)
-
程序块:本身含有局部变量声明的语句。程序块结构的声明作用域由最接近的嵌套作用域规则给出。可以嵌套,不能重叠。并列程序块不会同时活跃,变量可以重叠分配
活动记录的布局,局部数据的布局方式
活动记录
过程的活动所需要的信息用一块连续的存储区来管理,叫做活动记录或帧。上右图是一般的活动记录的域。不同编译器的域布局可能不同。
- 临时数据:保存临时值,如表达式中间结果
- 局部数据:作用域在本过程中的数据。通常按顺序连续分配,以便使用偏移查找地址。注意存在栈对齐
- 机器状态:过程调用前的机器状态信息,如返回地址、previous ebp等等
- 访问链:过程嵌套语言的非局部数据的访问
- 控制链:指向调用者的活动记录
- 返回值:存放被调用过程返回给调用过程的值
- 参数:存放调用过程中提供的实参
活动树、控制栈和运行栈
活动树
可以用活动树来描绘控制进入和离开活动的方式。例如下方这p个快排,我们一个主函数一共会调用2次函数,其中q函数又会调用p函数。
所以四个函数的具体调用顺序是:m -> r, q; q -> p。具体来说r和q是m的左右孩子,p是q的左孩子。
控制栈
当前活跃着的过程活动可以保存在一个栈中。这个栈就是控制栈例下:
此时控制栈的内容就是:m,q(1,9),q(1,3), q(2,3)
运行栈
运行栈是把控制栈中的信息拓广到包括过程活动的活动记录
我们由下向上来看,分别对应了我们之前看的那个活动记录栈
值调用、引用调用和换名调用
值调用
值调用指实参的右值传给被调用过程。也就是说对形参的任何操作不会影响调用者的实参的值
引用调用
引用调用把实参的左值放入形参的存储单元;若实参无左值,则将实参的值存入新的存储单元,并传递该单元的地址。对形参的任何赋值都会影响调用者的实参。
换名调用
考虑内联函数
TODO:左值与右值
例题
考虑下面值调用、引用调用和换名调用的输出分别是什么
值调用就是先把实参的值算出来,再拷贝给实参,理解成深拷贝。我们在函数中做的各种操作不会影响到被调用的值。所以答案是2
引用调用要求实参是个地址,对于a+b这种表达式,我们创建一个临时单元,让x指向这个临时单元。所以答案是2+1+2+3=8
换名调用是每次使用形参时,都回到调用点重新代入实参表达式。所以答案是9
第七章 中间代码生成
中间表示
抽象语法树和有向无环图(DAG)
这两者都是图形化的中间表示。我觉得画出的关键是对运算符的先后次序进行排序吧。
如果需要转换成中缀表达式,就中序遍历;转成后缀表达式就后序遍历。
后缀表达式
中缀表达式转后缀表达式可以使用 调度场算法,也可以先构造语法树然后再后续遍历语法树
三地址码
三地址码(TAC)的一般形式是
其中x、y、z表示名字、常数或临时变量。op表示运算符。
三地址码可以看作树语法树或DAG的一种线性表示。我们进行后续遍历,遇到运算符产生对应的三地址码
如何翻译成三地址码
声明语句
赋值语句
为了让名字直接出现在三地址码中,实际上应该把名字理解为在符号表中位置的指针。
具体来说哦我们使用 lookup 函数查询指针,并用 emit 输出对应三地址代码字符串
布尔表达式
这里的核心是短路运算。
我的一些理解是,如果不能短路那么需要生成一个新的出口。框内部的继承属性code输出当前true or false,依据短路运算来决定最终是否可以输出最终的结果还是进入下一个表达式。
B -> B1 or B2
B1.true = B.true;
B1.false = newLabel();
B2.true = B.true;
B2.false = B.false;
B.code = B1.code || gen(B1.false,':') || B2.code
B -> B1 and B2
B1.true = newLabel();
B1.false = B.false;
B2.true = B.true;
B2.false = B.false;
B.code = B1.code || gen(B1.true, ':') || B2.code
B -> not B1
B1.true = B.false;
B1.false = B.true;
B.code = B1.code
B -> (B1)
B1.true = B.true;
B1.false = B.false;
B.code = B1.code;
因为code是继承属性
B -> E1 relop E2
B.code = E1.code || E2.code || gen('if',E1.place, relop.op, E2.place, 'goto', B.true) || gen('goto', B.false)
B -> true/false
B.code = gen('goto', B.true/false)
控制流语句
我们将条件判断设置两个出口,分别是B.true和B.false。注意这些有关地址、行号的属性都是继承属性。
而B.code,S.code这类顺序执行块的三地址指令序列是综合属性。
三地址码中,newLabel()返回一个新的标号,而函数 gen()产生一个三地址指令或标号,并把这个三地址指令或标号的串值作为返回值
关于这一块的地址生成,我的方式是,从上到下(从B.true/begin,到S.next的上方,将每一个属性赋值决定,然后通过gen按从上到下的顺序输出即可)。
当然从上到下不是指顺序遍历。比如像goto ... 这种,就是说上面那一块,比如说S1,它的下一步,S1.next = xxx
S -> if B then S1
B.true = newLabel();
B.false = S.next();
S1.next = S.next;
S.code = B.code || gen(B.true,':' || S1.code)
S -> if B then S1 else S2
B.true = newLabel();
B.false = newLabel();
S1.next = S.next;
S2.next = S.next;
S1.code = B.code || gen(B.true,":") || S1.code || gen('goto',S.next) || gen(B.false,':') || S2.code
S -> while B do S1
S.begin = newLabel();
B.true = newLabel();
B.false = S.next;
S1.next = S.begin; //这句是关键,不要漏了
S.code = gen(S.begin,':') || B.code || gen(B.true,':') || S1.code || gen('goto',S.begin)
S -> S1; S2
S1.next = newLabel();
S2.next = S.next;
S.code = S1.code || gen(S1.next,':') || S2.codeuo
布尔表达式的控制流翻译
1. 尝试翻译 a<b or c<d and e<f
一般我们按照运算符优先级从低到高进行分区。如第一次分块我们按 or 分成如下结果:
![]()
然后接下来在B2中按 and 展开
![]()
if(a<b) goto B.true
goto B1.false
B1.false: if(c<d) goto B3.true
goto B.false
B3.true: if(e<f) goto B.true
goto B.false
通常我们将各个真假出口替换为Li
2. 翻译如下语句
while(a<b){
if(c<d) x=y+z;
else x=y-z;
}
画成图会好很多。依旧是分层,while分一层,然后if分一层
S.begin: if(a<b) goto B.true
goto S.next
S.true: if(c<d) goto B.true
goto B.false
B.true: t1 = y+z
x = t1
goto S.begin
B.false: t2 = y+z
x = t2
goto S.begin
S.next:
APPENDIX
Lex基础
- 翻译将Lex源程序翻译成一个lex.yy.c的C语言源文件,包含根据正规式所构造的DFA状态转移表,和驱动该表的总控程序yylex()
- Lex源程序包括定义段、词法规则段和辅助函数段三部分
- yyin和yyout代表输入输出,yytext指向当前识别的token的指针,yyleng指当前token的长度。
- Lex按最长匹配原则确定被选中的单词
- 如果一个字符串能被若干正规式匹配,则先匹配排在前面的正规式
YACC基础
- Yacc编译器将由规则生成的文法说明文件转换成C语言编写的语法分析器文件。此文件包括语法分析驱动程序yyparse()以及LALR分析表。
- Yacc中产生式的优先级与产生式中处在最右端的终结符的优先级相同
用YACC解决动作冲突
-
对于归约-归约冲突,Yacc优先于先出现的产生式(写在前面的产生式)
-
对于移进-归约冲突,Yacc优先于移进
- 如果归约的产生式的优先级高于移进的终结符的优先级,则进行归约
- 如果归约的产生式的优先级和移进的终结符的优先级相同,且产生式左结合,则进行归约。反之如果右结合就进行移进



是应该移进还是归约呢?如果不看推导的话,无论这里移进还是归约最后都可以化简为接受状态,这里就是典型的移进归约冲突。原因是因为语法具有二义性,同时分析器只具有局部视野。
浙公网安备 33010602011771号