说实话,我不是很想上这门课,确实没什么大用,虽然我觉得这门课学一学也挺好,但是我觉得弄8个大实验就真的不太够意思,不如纯理论。
吐槽归吐槽,马上就要考试了,能学多少是多少(学到了语法制导翻译的前半部分),最后考试感觉差不多能及格,这我就满意了。
虽然很多人说这个老师是念ppt,但是其实不然,ppt很复杂,你自己看还是有点麻烦,不如让老师给你指出顺序,这样也舒服,建议2倍速。
这课讲的确实不错,条理清晰,ppt也好看,果然群众的眼睛是雪亮的,听学校的课真不如课下听听这个。
编译就是一个翻译的过程,将高级语言(源语言)翻译成汇编语言或者机器语言(目标语言)。
编译系统是一个很大的项目,从源程序到目标及其代码全部包揽,其中分为4部分,以C语言为例解释其作用:
#
号语句。比如#include语句(导入模块),处理宏定义#define编译器
:.i变.s,编译系统的核心,编译原理课程研究的对象
我们来具体研究一下编译器的编译过程,整体上分为两个阶段,先从源语言理解出语义,变成中间代码,然后再将中间代码转化为目标的语言。这两个阶段分别叫做编译器的前端和后端。
整体过程其实和人类的翻译过程是很像的:
词法分析 — 单词词性,孤立个体
语法分析 — 单词组合,构造短语
语义分析 — 连词成句(纠错),直译句义
中间代码 — 抽象提取,句义意会
代码优化 — 同义替换,翻译润色
目标程序 — 翻译结果,成品输出
下面来逐一介绍:
词法分析(Scanning)就是把一整个句子,切成一个又一个单词,确定单词的类型并且打上标记,变成token
一词一码的单词,只需要种别码就可以了,属性值可以不要,因为已经可以区分了。对于多词一码的,需要用种别码界定大类,然后用属性值确定内容。
语法分析(Parsing)将词法分析的结果加工,把词串成短语,以语法树的形式输出。
语义分析(Semantic)比较抽象。
什么是语义呢?TODO,我感觉程序说白了就是在玩各种变量,函数,那么语义就代表着我要把变量的各种信息以及函数的各种信息都提取出来,只要把这些东西搞出来,后面转化成机器语言就很方便了。
所以语义分析的一大任务是收集标识符
的属性信息,标识符无非就是变量函数这些东西,收集他们的属性就是在搞清楚他们想干点什么。
最后这些东西记录在符号表里,之所以NAME字段是<符号表中地址,长度>的格式,是为了节省空间,毕竟长度不确定,统一长度难免浪费。
除此之外,语义分析还要做最后一步的正确性检查,确保在进行代码生成前没有错误。
中间代码生成(IC-gen)。语法树其实已经是一种中间形式了,但是这种结构还是更适合人看,机器更喜欢一条一条的代码,所以ic-gen阶段将语法树转变为一条一条的中间代码,后面生成目标代码可以直接对照生成。
中间形式叫三地址吗
,代表一条指令最多有三个操作数,一般用四元组表示,多出来的这个1指的是操作符,很像汇编语言。
给一张表,下面的地址码使用标志符表示的,不是说地址吗?这是因为,符号表里面已经记录了标志符到地址的映射,所以用标志符=用地址。
总结一下四元组表示的规律:
目标代码生成(nc-gen)是吧中间形式映射到目标语言。一般是将中间代码映射到目标代码,有的人也会把语法树映射成目标代码,但是及其受限,还是要一步一步走才好。nc-gen的关键是寄存器的合理分配。
代码优化无非就是优化空间或者速度。
字母表:符号的集合
,一个语言中最最基本的元素集合。注意,字母表里面不一定只是一个一个的符号,也可以是串,后面就会看到的。
字母表的运算如下:
字符串是一个老生常谈的概念,其实就是字母表上字符的任意有序排列组合(包括空串),用规范的语言说,就是字母表克林闭包的元素。
字符是语言中最基本的元素,那串就是语言中最常用的基本元素,字符本身其实也算是一种串,串是一个更大的概念。
其实到这里你就可以发现,这两个东西似乎有一种本质的联系,说白了就是集合和元素的关系。
你会发现,串的连接运算与字母表的乘积对应,串的连接结果 ∈ \in ∈字母表乘积。串的任意组合对应着字母表的乘积组合,任意串对应着字母表的克林闭包。
其实就是元素和集合的关系啊,有趣。
什么是文法?用抽象一点的话说,就是表示了基本符号
如何构成语法成分
,以及语法成分如何进一步组成更宏观的语法成分。
蚌埠住了吧,直接看例子吧。对于自然语言,基本符号是单词,语法成分是短语和句子。倒数4个介绍了基本元素如何构成语法成分,而前几条介绍了语法成分如何组合成更宏观的语法成分。
想一想语法树,是不是一层一层的,我们这个构成其实也是从下到上一层一层的。
下图仅仅是针对自然语言的,基本符号是单词,语法成分是短语和句子,如果是针对单词来说,那么基本符号就是字母,语法成分就是单词。
为了便于计算机处理和理解,我们用数学语言(形式化语言)去描述文法。总的来说,一部文法规定了一个语言的各种成分,以及转化规则:
一部文法的元素集合其实在产生式里面就可以看出来,所以不引起歧义的情况下,甚至只写P都可以。所以我们重点研究一下产生式的表示:
这是最基本的写法,左边到右边是一个箭头,从左到右是分解,从右到左是合并。
P还可以继续简化,相同左部的产生式可以合并为一个,右边用|
隔开,代表或,右边的每一项都叫候选式。
最后说下符号约定,好复杂,我感觉不用记那么多,后面慢慢就都记住了,所以这里只把基本规律放出来:
什么是语言?此处直接给出定义:句子的集合
听起来有点像字母表,其实字母表也算是一种很简单的语言,而我们的语言集合,可以是无穷的集合,因为语言集合是从文法推导出来的,可以说,文法与语言是对应关系。
如何判断一个句子是否属于一个语言呢?还是要着眼于文法。一条文法,从左到右是分解,我们这里叫做推导
,从右到左是合并,我们叫规约
。
直接看例子吧。
我们说,终结符构成的串就是语言的一个句子
,那我们在推导过程中产生的中间形式,里面又有终结符,又有非终结符,甚至全是非终结符,这又叫什么呢?句型
句型是比较抽象的,不具体的,所以就叫“型”,而句子就是一个完全的终结符串,句子也可以理解为特殊的句型。
回归语言的定义,语言和字母表的元素都可以是字符串,那他们到底不同在哪里呢?本质在于,字母表是我们直接给出的集合,而语言是通过文法推导出来的。
所以我们说L(G)是文法G生成的语言L。给定一部文法,很有可能生成一个无穷集合,这就是用有限的文法表示无穷的句子,这就是语言的本质,用元素+规则表达无穷的组合。
我们来具体举几个例子。下面这部文法表示的是标识符,4条文法就表示出了标识符这个语言,而我们知道,标识符是无穷的:
最后再重申一下语言和标识符的关系,区别仅仅在于怎么生成的,他们从成分上来说,其实是一样的,或者说字母表本就是一种语言
。由此,运算其实是可以套用的,甚至可以把字母表通过语言的运算转化成更复杂的语言:
由此,串的运算,集合的运算,文法的推导,这三个东西其实是统一的。
0123四类文法,自由程度依次递减。
0型文法就是我们前面用的文法,叫做无限制文法,高度自由,左边只需要有一个非终结符就行了。
这是最基本的要求,毕竟文法从左到右是推导,你左边要是没有东西(非终结符)可推导,这也说不过去,所以虽然高度自由,但是至少得有一个非终结符。
1型文法叫上下文有关文法(CSG),在0型文法的基础上,保证上下文不变,总长度只增不减。
上下文不变还是比较好实现的,而为了保证总长度只增不减,CSG生成式的右边不能有空串。
2型文法叫上下文无关文法(CFG),是基于1型文法的。
看似有关和无关冲突,但是如果我们限制上下文为 ϵ \epsilon ϵ,1型文法就会变成2型文法(不考虑右边为空的情况)
2型文法除了上下文为空以外,相比于1型文法,右边是允许空串的。
一般来说,我们写的文法更像是2型文法。
3型文法叫正则文法,也叫线性文法,分为左线性文法和右线性文法。
之所以叫线性文法,是因为每次推导只会增加1的串长。左右代表着字符串的增长方向,比如右线性文法,增长方向和非终结符的位置一致,就是右边。
正则文法和正则式对应,我们平时编程语言中会接触很多这个东西。
最后,如果不考虑1型文法右边不允许空串,那么0-3型文法就是逐级限制的。
前面我们知道,我们一般都是用2型文法的,所以就针对CFG研究一下它在编译过程中的表示。
一颗CFG分析树有三种节点,分别为根节点,内部节点,叶节点。
把所有叶节点排起来,直观看这些叶节点整体就是一棵树的边缘
,这代表了一个树的产出
给定一个推导,推导的过程中,句型会不断变长。对应到树这里,给定一个根节点,每推导一次,树就向下生长一点,边缘/产出就会增加,推导的过程就是树不断变大的过程。
所以可以说,每个句型
都可以对应一颗分析树
注意,叶节点不一定是终结符,这说明我们的推导不一定非要推导到没有非终结符为止,提前停下来也可以,也算产出。
下图中,该句型有3个短语,其中,有一个是直接短语。
因为产生式是一对多的,就是一个左部可能有多个右部,所以只能说推导出来的右部一定是直接短语,不能说某一个右部就是直接短语。
直接短语一定是当前句型可以推导出来的右部,随便给一个右部,不一定是当前句型能推到出来的,可能是另一个句型的直接短语。
最后说一下二义性,一颗语法树代表一个语法结构,如果同一个句型可以得到两颗不同的语法树,计算机就会出现理解的歧义,计算机中不允许歧义出现。
对于一个CFS,没有一个充要条件可以判断二义性,但是有充分条件可以判断二义性。
如果出现二义性,解决办法就是修改文法。
铺垫了这么久,终于到了词法分析了,不过上来还是得学一堆数学。
词法分析用到3型文法,之所以不用2型,是为了节约成本,所以要先学正则式。
前面学过正则语言,本质上还是语言,是集合。但是这种表示方法比较麻烦,正则式是正则语言的紧凑表示法。
一个正则式表示与其形式匹配的字符串,所有串的集合就是该正则式对应的正则语言,所以有L(r)的写法。
正则表达式的定义是递归定义:
正则运算规则
组合成的新式子也是正则式其实正则运算规则和集合的运算是一一对应的,因为正则式本来就代表着语言(集合),正则式和正则文法一一对应,下图给出了对应关系。
下面这些代数定律,其实对应离散数学里的集合的运算定律,不用刻意去记。
回归编译过程,正则式的意义在于,可以定义一类单词,所以本节给出各类单词的正则定义。
可以看到,正则式和文法的写法几乎一样,毕竟本来就是等价的,只是写的更加简略罢了。
FA是MeCluoch和Pitts提出,这俩名好熟悉啊,去一搜才知道也是人工智能领域的老祖级别的任务。
人工神经元的提出者
FA的特点是,给定有限个状态,只需要不断输入,状态就可以不断切换,并且给出输出,学过数字逻辑或者计算理论的人会很熟悉自动机这个东西。
FA可以用转换图表示,圆圈是状态,有向边代表输入与转换。
FA如何识别语言呢?给定一个起始状态,然后不断输入每一个字符,状态就会不断转换,输入完毕以后,如果停留在接受态,那么就是接受了。
此外,FA有一个最长子串匹配原则,下图中,如果输入<=,那么优先匹配<=,而不是<
确定有穷自动机,即DFA。
状态,输入,转换,这三者决定了DFA的基本框架。初始态和接受态决定了我们要接受什么串。
DFA也可以用转换表来表示。DFA的显著特征是,转换图中同一个状态不会有两条相同的输入边,转换表中一个格子里面只有一个目标状态。
最后给出DFA的算法实现,还是挺简单的。
不确定的有穷自动机,即NFA
NFA和DFA的唯一区别就在于,一个状态,一个输入,可能完全走向两个结果,具有不确定性。
举个例子,你可能一下子无法接受下面这个图,0状态对于a输入有两个可能的转换。其实没什么问题,这个是NFA,我们再数字逻辑里学的是DFA。NFA是不确定的FA,有二义性,怎么走由你自己来判断,又或者交给一定的规则和概率(计算机实现)
NFA的特点在下图也可以看出来,转换表里面,一个格子里面,目标状态可以是一个集合。
从前面以及下面这个图可以看出来,NFA虽然直观,但是有二义性,DFA复杂,但是适合计算机实现,没有歧义。
一个好消息是,NFA和DFA是等价的,这也就意味着,人可以先写出NFA,然后通过一定的算法转化成DFA,交给计算机去实现。
另一个等价性是,带空边的NFA和不带空边的NFA也是等价的。
所以我们写NFA的时候更加自由,可以带空边去写,最后逐步转化成DFA就可以。
整体流程:
大致规则如下,转换的时候,按照正则式的优先级进行转换。
再举个实际的例子,在实践的时候,其实拆解顺序反而大致与运算优先级(括号,*,与,或)是相反的,从外往里拆。但是请注意,拆解顺序≠优先级,所以仍然是保持着优先级的。
在这张图里,按照括号先拆成3节,之后每一节内部再拆。具体拆解顺序还是有一点玄学,需要多练,但是总的来说意思不能变。
最后插播一个NFA转RE的例子,虽然现实中没人会这么做,但是保不准考题会这么考,看情况可以跳过这部分:
拓广
,拓广在左右两边放了两个绿色的圈儿,用epsilon弧链接附近的节点。之所以这么干,是为了统一初态和终态,很合理。首先介绍两个运算:
自反闭包
:写法是 ϵ − c l o s u r e ( 集合元素 ) \epsilon-closure({集合元素}) ϵ−closure(集合元素),作用是消去 ϵ \epsilon ϵ弧,得到自己+只输入 ϵ \epsilon ϵ就可以走下去的路径上的所有字母。求NFA新状态集
: I a = { 集合元素 } a I_a=\{集合元素\}_a Ia={集合元素}a。对于状态集I要做两件事:
举个例子:
了解了两个基本运算后,就可以开始应用到子集法中了。子集法的原理是将NFA图转换成状态矩阵,把矩阵再转换成DFA。因为我们用状态机,只关心输入和输出,其中的状态到底代表什么,我们其实是不关心的,我们只关心状态转换。因此我们完全可以把状态集看成一个状态。这是子集法的核心思路。来看个例子:
之后就把状态集标号,用新的号作为新状态,如此NFA就变成DFA了。
注意,NFA确定化不能改变NFA的计算能力,但是可以为下面的简化做铺垫。
DFA最小化能够真正地降低计算成本。
什么是可分呢?其实就是状态集内部的状态不完全等价。比如{2,3,4,5},这个状态集内部23等价,45等价,那么这个状态集就可以分成{2,3},{4,5}两个新状态集,然后你再去新状态集内部判断装态是否完全等价。
看下面这个例子:
状态集
,因此也是等价的,不可分的。所以再次强调,可不可分取决于结果的状态是否在同一个状态集,而不是结果的状态一样。
上面这个太简单,上点强度,看这个文章里的例题。万变不离其宗,关键要明白,结果只要在一个集合里就行,不一定要完全一样:
dfa最小化
如果能一直走下去,那么DFA就是正常识别。
如果走到某一步,其状态和输入对应的下一个状态是空,且当前状态非接受态,那说明走不动了,此时就可能有问题:
自顶向下和自底向上
最左推导:选择最左非终结符
最右规约:合并最右终结符
反过来类似,最右推导,最左规约,这两个都是规范的。
推导是无穷的,但是最左/最右是唯一的。虽然最右推导是规范,但是自顶向下的语法分析采用的却右是最左推导。
其实也合理,就是进来一个字符,我就进行一系列推导先推出这个字符。
递归下降分析。可以思考一下过程。
自顶向下的过程比较机械,不是任何文法都可以用自顶向下分析的,如果不合适,就需要转换文法。
间接左递归的原因在于,第二个产生式里面有S打头的候选式,我们把这个候选式消灭就可以。具体消灭就是代入,此时又会产生直接左递归,进一步消灭一下就好。
回溯影响效率,不如提前预测,LL(1)文法具有这个能力
不允许空产生式。所以这你玩个锤子,不够灵活。
允许空产生式了,但是有一些问题需要解决。
什么时候才能用空产生式?我们用空的目标是为了在后面碰到当前的输入字符,所以我们首先要直到,当前非终结符A的后继符号集FOLLOW集
我们进一步优化,把非空也一起带上,给产生式定义一个可选集(这其实就是他可以读取的输入)
SELECT集。
这个东西就叫q_文法,同S_文法一样,都是确定的,但是适用范围仍然有限。
LL(1)文法允许右边候选式以非终结符打头,更复杂了,所以又得引入新东西。
同一非终结符的各个候选式的SELECT集不可相交,这样就不会出现不确定性。
这三个东西感觉容易混,特此强调。
FIRST集是针对LL(1)文法的,LL(1)文法的SELECT集看似和q文法不同,但是其实是向下兼容q文法的。在有空候选式的情况下,考虑FOLLOW集,否则就直接看FIRST集。
因为q文法比较简单,候选式开头一定是串首终结符,所以其FIRST集直接看候选式开头就可以。而LL(1)文法比较复杂,还得推导一下。
举例子。首先明白,FIRST集代表可以推导出的所有可能的串首终结符集合。
下图中,245这三个,串首终结符是固定的,FIRST集已经确定。1依赖3,3又依赖5,所以直接把5的给3,3的给1。
这只是一个简单的例子,但是阐述了一个核心道理:具有依赖关系的非终结符,被依赖的那个非终结符的FIRST集,可以直接给依赖者。
给出复杂的算法,这是一个递归算法,核心就是那个依赖关系。考虑到依赖关系,你这个算法得迭代多次,直到一轮下来没有变化为止。
注意,只有当X完全推出 ϵ \epsilon ϵ,此时 ϵ \epsilon ϵ才能加入到FIRST集中,如果后面还有东西, ϵ \epsilon ϵ开头算不得串首终结符。
再拓展一下,把一个非终结符扩展为一个串,其实还是依赖关系。
计算FOLLOW集之前,应该先算出FIRST集。
先把语句结尾放到开始字符里面,之后从上往下扫描,观察产生式的右部中的每一个非终结符。
终于到了最关键的地方了,SELECT集计算出来以后,如果相同左部的不同产生式SELECT集不相交,就可以针对每个非终结符,指定可以捕获的输入了。
说白了,就是利用已经计算好的,非终结符的FIRST集,去计算生成式右部串的FIRST集,如果FIRST集里没有空串,那么就直接用,否则还要带上左部的FOLLOW集。
上述分析后,可以得出,相同左部的不同产生式的SELECT集不相交,所以是LL(1)文法。构造预测分析表(是不是很像状态矩阵),每一个格子都是确定的。
首先要有一张预测分析表,这是所有LL(1)文法分析的基础。
首先是主程序,读取一个TOKEN,然后从顶层分析函数开始递归过程,最后完成后,读取一下是否是EOF标记,如果是,那么就接受,否则就出错。
之后就是每个文法具体的递归处理函数了:
将文法的右部词语按顺序分成两类:
由此,函数去调用函数,程序就会如同语法树一样从上而下展开。
下推自动机类似于FA,但是更牛逼,因为有一个栈专门用于储存信息。
下面那个例子,给FA去识别是比较难的,因为他记不住你读了几个a,只能通过增加状态数量来代表读了几个a,很有限。而下推自动机,可以在读a的时候,不断压栈,直到读了b,用b和栈顶的a去配对消消乐,如果读到最后栈里面元素为空,那ab数量就恰好相等了。
过程还算简单:
其实递归预测没啥优点,所谓的直观性,谁去看你的代码啊,没啥用,还是非递归的好用。
预测的过程中还可能发生错误。
说白了就是在分析表里的格子是空的,此时编译器认为走不下去了。但是实际上,如果可以牺牲一个非终结符,用剩余的部分推导出完整的输入,也行。凭什么你就说弹出这个非终结符,剩余的部分就可以匹配当前输入呢?
就在FOLLOW集里,FOLLOW集代表当前非终结符后面可能跟随的终结符,如果这个终结符和输入匹配,那就可以继续走下去了。
自底向上和自顶向下反过来。我们再自顶向下分析中,采用最左推导,即每次针对最左边的符号进行推导。而在自底向上分析中,采用最左规约,看似都是左,其实最左规约是最右推导的逆序,是把已经读入的字符串的右部规约。
这个过程有点像,算法题里的括号匹配,只是我们这里的匹配规则比较复杂,要通过文法来匹配,匹配到的右部叫句柄
自顶向下的关键问题是,选择哪个产生式,而移入规约框架的关键问题类似,是选择哪个句柄进行规约,正如我们前面所说,一个产生式的右部,不见得就是当前给定输入句型的短语。
LR中的R,指的是最右推导序列,与我们使用的最左规约是等价的。
有了表以后,分析的还是很简单的,难点在于这个LR分析表从何而来。
这个点,左边的是已经进栈的,右边的是期待进栈的,因此每个产生式都有若干个状态,这若干个状态相邻的都是后继项目
,每个状态都是LR(0)分析的条目
这么多状态的转换,是比较复杂的,可以把一些等价的状态合并,变成项目集闭包。
首先画出状态图。以 I 0 I_0 I0举例:
扩充闭包
:从起始状态开始,对新的状态进行闭包扩充
求新状态
:对一个状态扩充完毕后,就可以准备迈向下一个状态了
规约状态
,没有边引出非规约状态
,给定一个输入,就可以引出一条边,边上就是点后面的符号,即我们期待的符号,然后我们可以得到项目的后继项目。后继项目不一定只有一个,如果有多个项目期待同一个输入,那么就去往同一个新状态。
之后根据状态图写表:
我们前面还没有考虑到冲突的情况,我们说,当一个状态里,只有一个规约项目时,该状态为规约状态。那么同时有一个规约项目和非规约项目时,就会冲突,我到底是规约还是移入呢?
所以第一种冲突叫移进规约冲突
还有一种叫规约规约冲突
。这个就更离谱了,如果一个状态里面同时出现两个规约项目,就会冲突,比如下面状态2的B和T
SLR——Simple LR(1),用于解决LR(0)的移进规约冲突和规约规约冲突。先介绍一下SLR分析的基本思想:
对状态2来说,发生了移进规约冲突。这是因为LR(0)太过短视导致的。如果我们把T规约成E,你会发现,E的FOLLOW集里没有星号,也就是说,如果你选择规约,那么后面如果出现星号,这个文法就会出错。
所以在分析的时候,可以参考FOLLOW集与下一个输入的关系。
具体怎么做呢?首先要确定一个前提,就是1个可移进集合与n个规约项目FOLLOW集,这n+1个集合互不相交
。所谓可移进集合,指的是每个项目的点后面紧跟的符号,构成的集合。
之后,根据输入符号进行操作:
SLR分析是基于LR(0)分析的,对LR(0)分析表的冲突项目进行分析与调整,这就导致,分析表的规约状态里面,不一定都是规约动作了。或者说,这个状态也算不得完全的规约状态。
下面来个例子,状态2有冲突,其中两个规约项目的FOLLOW集列出了,而移进集合为{a}。由此,直接判断输入为a时,移进,b和$规约到T,d时规约到B,这样,移进规约冲突和规约规约冲突就都解决了。
当然,SLR也可直接嵌入LR(0)分析中,只需要略加修改即可,即碰到规约项目时,并不是无脑规约了,而是输入在FOLLOW集才规约。
SLR分析的大前提是,那几个集合不相交,但是如果仅仅利用FOLLOW集的信息,还是有可能满足不了这个前提,所以需要用更多的信息,就是我们的老朋友:FIRST集合。
FOLLOW集是规约的必要条件,只能代表在一个文法中,这么规约以后,后面可能碰到的第一个非终结符。很明显,我们是分析特定句型,单纯用FOLLOW集不适用于特殊情景,即FOLLOW集不是规约的充分条件,所以需要收缩集合,此时就要用到FIRST集合与展望符。
规范LR(1)项目是一个LR(0)项目+一个展望符。展望符只是用来为规约服务的,告诉你规约以后后面必须紧跟哪个终结符,如果这个项目不是规约项目,那么就没用,这个时候就看那个可移进的符号。
同LR(0)项目,LR(1)项目也有等价项目,在扩展闭包的时候要用。等价项目的生成式很容易写,但是展望符怎么得出呢?
就用FIRST集,比如下面,等价项目的左部是B,那么其后面紧跟 β \beta β,这个东西的FIRST集元素就是展望符,这个就是自生展望符,当然,考虑到他可能是空集,所以最坏的情况就是继承A的展望符。
其实这里的FIRST集很好算,你就看 β \beta β的串首终结符就好,因为我们只要一个。空就继承。
写LR(1)自动机图的过程和LR(0)基本一致,区别在于:
上图中,有一些状态,除了展望符两两之间完全一样,那么这些状态就是同心的
。LR(1)本质上是通过用展望符,将状态分裂,细化,这么做的缺点是状态变多了,甚至翻倍。
然而这么做除了让得出自动机更方便以外,并没有什么好处,同心状态其实是可以合并的,因为其左边的行为完全一致。
LALR可能会产生规约规约冲突,不过不会产生移进规约冲突,因为我们只是去合并展望符,展望符只在规约时起作用。
同时,LALR虽然可以节省空间,但是可能会推迟错误的发现。
LR(0)给出了最基本的扩展闭包,以及画自动机图的流程。
SLR加强了LR(0),但是很有限,LR(1)大大加强,但是消耗资源很多,LALR(1)缩减了LR的同心状态,分析效率会弱一些。
不过貌似实际上SLR考的比较多,可能是比较简单。
略。
语法,语义,中间代码生成,这三个东西很容易就可以放到一起,即SDT(Syntax-Directed Translation)
SDT使用文法来指导翻译。
首先明白什么是语义,计算机里的语义,就是词的各种属性值,我们求解语义,其实就是在计算各种词的属性。
所谓的语法制导翻译,就是让语义在语法分析的过程中,顺带被一起计算了,为了打成这个目标,需要把语义和语法绑定,也就是下面的SDD。
SDT是SDD的具体执行细节,是把语义规则嵌入到产生式里面。
举个例子,看第一个产生式,他的意思是,先解析出T,然后把T.type赋值给L.inh,之后再把这个属性传给推导出的L。
综合属性。节点值依赖子节点。其对应的符号位于生成式左部
继承属性。节点值依赖父节点,兄弟节点,N本身的属性。其对应的符号位于生成式右部。
下图是带有属性值的分析树,叫做注释分析树
。下面的文法只有综合属性,所以又叫属性文法
,是单纯用来计算属性值的,没有副作用。
下图这个SDD树就是有继承属性的(也有综合属性)。
从前面的图大概也可以看出来,属性值的计算要通过依赖关系实现。依赖图
有点像注释分析树,结构一样,只是没有标出属性值。
求值的顺序应该是这个有向图的拓扑排序。只有综合属性就自下而上就行,很简单,但是如果还有继承属性,就需要拓扑排序,即使如此,也不一定能保证一个SDD一定没环。
S-SDD就是只有综合属性。
L-SDD是S-SDD的扩充。
L-SDD允许继承属性,但是对继承属性有严格限制,L-SDD适合自顶向下的LL分析,所以计算顺序也是从左到右。
下图中,第五个产生式,Q.i依赖于R.s,这是右兄弟,不符合L-SDD。
SDT将生成式与语义表达式结合起来,规定了计算的具体顺序。当进行语法分析的时候,只需要按照SDT规定的计算顺序,就可以得到目标的语义。
首先确定这个文法是LR文法,将S-SDD转换为SDT。
然后构造文法的自动机。
还需要做的一件事就是给属性值扩展栈的空间。一般就是一个属性,多属性可以多开点空间或者用指针,写的时候不影响。
之后针对新开的栈,把抽象动作具体化。
最后就是执行LR分析,跑自动机图就可以,每次规约的时候,就进行一次SDT操作。
最后就可以得到属性值。
同样,先把生成式和动作合并:
然后判断一下这个文法是不是LL文法,具体判断就是计算SELECT集。如果是LL文法,那么就可以使用三种方法实现SDT。
如果只是为了应付考试,学一种就差不多的了。