编译原理小结——字符和字符串之间的游戏

来源:互联网 发布:jabra elite sport软件 编辑:程序博客网 时间:2024/06/07 00:07

前记:第一次使用LaTeX编辑公式,这个东西不错~让我意识到了转义符号’\’的强大威力!
吐槽:本人使用清华大学出版社的《编译原理》第二版,这本书真TM抽象,各种乱七八糟的形式化定义一个接着一个,看得我吐血了。考前突击,花了四天时间硬是把这本书的核心部分啃下来了,写下了8千字的”小”结~
《编译原理》(清华出版社)教材链接


1 词法分析 lexical analysis

词法分析是编译的第一个阶段:主要是从左至右逐个字符地对字符流的源程序(比如.c 和 .cpp)进行扫描,产生一个个单词(token)序列。简而言之:读入源程序,输出单词符号。单词符号是一个程序设计语言的基本语法符号,一般分为以下5种:

  1. 关键词:for, while, if, else, double, int
  2. 标识符:自己声明的变量、类/对象
  3. 常 数 :”hello, world”, 12345
  4. 运算符: >, =, ==, +
  5. 界 符 : 逗号,分号,花括号

按理说,词法也是语法的一部分。不过编译器把词法分析从语法分析中独立出来,是因为有些好处:

  1. 使整个编译程序的结构更加简洁、清晰、条理化
  2. 编译程序的效率会改进
  3. 增强程序的可移植性

词法分析做的一些细节工作:滤掉程序中的注释和空白(空白即空格、换行符、制表符) ;记录读入字符行的行号(比如编译器会提示error在第几行); 完成预处理,比如C/C++的#defile

正规集有两种表示工具:正规文法(3型文法)和正则式,正规集是正规文法所定义的语言和正规式所表示的集合。两种表示方法是等价的,可以互相转换。正则式即正则表达式,它由递归定义:首先设字母表是,辅助字母表是={  , ε ,  ,  , ( , ) }

  1. ε都是上的正规式,它们表示的正规集分别为{ε}
  2. 任何 a,a 是上的一个正规式,它表示的正规集为{a}
  3. 假定e1e2都是上的正规式,它们表示的正规集分别为L(e1)和L(e2),那么 (e1) , e1e2 , e1e2 , e1 也都是正规式,它们表示的正规集分别为 L(e1) , L(e1)L(e2) , L(e1)L(e2) , (L(e1))
  4. 仅由有限次使用上述三步而定义的表达式才是上的正规式,仅由这些正规式所表示的字集才是上的正规集。可以看出经过递归调用,正规集 L“越来越大”

有穷自动机是一种识别装置,可以准确的识别正规集。分为两类:NFA(Nondeterministic Finite Automata,不确定的有穷自动机) 和 DFA(Deterministic Finite Automata,确定的有穷自动机) ,它们都可以用五元组 M(K,,f,S,Z) 表示。K 表示状态的有穷集,表示字母表,f 表示状态转移的方法,S 是初态,Z 是终态。NFA 和 DFA的差距主要体现在 f 上面:DFA的 f 是“确定的”函数,某一个状态,输入某个字符,可以确定转移到哪一个状态上面去;NFA 的 f 的状态转移是不确定的映射,某一个状态,输入某个字符,可能转移到很多个状态上面去,不确定到底转到哪一个状态去。S 也有一点区别,DFA的 S 是唯一的,NFA 的 S 可能有多个。

NFA通过子集法可以转换到DFA,子集法的要点是掌握两种运算:闭包运算和弧转换运算。这里必考大题:正则式 –> NFA –> DFA,主意事项:1.确定化的时候不重复,不遗漏;2.假设NFA中的终态是Z,Z可能出现在DFA的多个状态中,那么这些出现过Z的状态都是终态,所以DFA中的终态可能会有多个;3.在“填写表格”(确定化)的时候,别忘记闭包了(虽然一些题目没有涉及ε),DFA的初态就是NFA初态的闭包,后面是不断先弧转换再闭包;4.DFA的初态的前面,不要忘记 。可能会遇到DFA最小化的题目,此时采用分割法:首先分成终态和非终态两个状态两个子集,然后不断尝试分割这些子集,直到不能继续分割为止。


2 语法分析 grammar analysis

1 概念

一个程序设计语言是一个记号系统,它的完整定义应包括语法和语义两个方面。所谓语言的语法是指一组规则,用它可以形成和产生一个合适的程序。语法定义什么样的符号序列式合法的。

重要概念和定义:

  • 字母表:元素的非空有穷集和。字母表中的元素称为符号,字母表又称符号表,一般用表示
  • 符号串:由字母表中的符号所组成的任何有穷序列。
  • 符号串集合:若集合A中的一切元素都是某字母表上的符号串,则称A为该字母表上面的符号串集合
  • 闭包:上的所有有穷串的集合 。=012...n...
  • 正闭包:+,在中去除0即可
  • 规则,又称产生式,形如 αβ 的有序对,读作“定义为”
  • 文法 G定义为四元组(Vn,Vt,P,S)Vn是非终结符集;Vt是终结符集;P是规则(αβ)的集合;S是开始符,是一个非终结符。VnVt互斥,两者的并集等于文法G的字母表。S要求至少在一个规则中作为左部出现
  • 推导:结合上面的两个定义,又设γ,θ分别是V中的任意字符串,满足v=γ α θ, w=γ β θ,则v(应用 αβ)直接产生wwv的直接推导,w直接归约到v,记作vw。如果v经过“多次直接推导”产生w,则v+ w(这个’+’在符号上面,如果是’*’的话,这可能v=w,即根本没有推导)
  • 开始符 S 经过文法G[S]的不断推导,产生了句型(一个句型 = 对应语法树的叶节点从左至右串在一起,包含Vn);句型如果全部由Vt里的元素组成,则变成了句子;所有的句子便成为了语言L(G)。文法描述的语言,是该文法一切句子的集合

文法分为四种:0型文法、1型文法(上下文有关 context sensitive)、2型文法(上下文无关 context free)、3型文法(正规文法)。4个文法的差别在于对产生式施加的限制不同,具体的说,是限制逐级上升。0型文法的能力相当于图灵机,2型文法的能力相当于下推自动机。

其中上下文无关文法有足够的能力描述现今程序设计语言的语法结构,特点是:其产生式的左部有且仅有一个非终结符,这是课本的缺省文法。语法树(推导树)可以直观的描述上下文无关文法的句型推导过程。如果在任何一步αβα,β是句型,都是对α的最右边的非终结符进行替换,则称这种推导是 最右推导,又称规范推导。

二义:顾名思义,一个句子有两种意义。如果说一个文法存在某一个句子对应两颗不同的语法树,则说此文法是二义的。本人总结:依据课本上的例子,消除二义性可以通过“增加新字符”解决。本人脑补:程序语言之所以都是英文的:一方面是计算机诞生于美国,而美国的母语是英语;另一方面是因为英语的二义性很低,相比较之下,中文的二义性比较高,导致中文很难成为一门程序语言的载体。

短语:设G是文法,S是开始符号,αβγ是句型。如果S αAγ+ αβγ,则称β是 句型αβγ相对于A的短语。特别的,如果S αAγαβγ,则称β是 句型αβγ相对于Aβ直接短语。一个句型的最左直接短语,称作句柄。本人总结:在一棵语法树中,非叶结点有多少次”展开”,就有多少个短语;直接短语的特点是非叶结点直接全部展开到叶节点上面。


2 重难点

首先词法分析输入字符流,得到单词序列(本质上也是字符流,只不过进行了处理)。然后句型分析是输入单词序列,识别它们在语法上是否正确,这是语法分析的核心部分。课本上一律采用 从左至右的分析方法。句型分析分成两大类: 自上向下、自下向上。考试中,这里肯定考大题。

  • 自上向下:从文法的开始符号出发,反复使用各种产生式,寻找“匹配”于输入符号串的推导。
  • 自下向上:从输入的符号串开始,逐步进行归约,直到归约到文法的开始符号。

1 自上向下

如果一个文法满足LL(1)文法,那么其自上向下推导一定是确定的。自上向下推导如果不是确定的,则只能用带回溯的分析方法(效率低,代价高)。为了弄清楚LL(1)文法,首先要引出两个重要概念。设G(Vn,Vt,P,S)是上下文无关文法。AVn

  1. FIRST(A)={aA aB,aVt,A,BV}。如果A ε,则εFIRST(A)
  2. FOLLOW(A)={aS μAβ,aVt,aFIRST(β),μVt,βV+}, 如果β ε,则 #FOLLOW(A)。’#’ 是输入串的结束符

定义有一点复杂,总结一下核心。相同点是:FIRST,FOLLOW集的元素一定Vt;不同点是:FIRST(A)是从A本身的推导出“第一个终结符”,而FOLLOW(A)是从A的“下一个符号”推导出的“第一个终结符”。可以看出,后者的定义依赖于前者的定义。

本人做题教训:我开始以为课本上的aVt条件是无用的,误以为如果a属于FIRST集,就一定属于Vt。现在发现这个条件不可或缺。因为这个条件如果缺少的话,会错误地导致ε出现在FOLLOW集中。做题发现,ε不可能出现在FOLLOW集中,因为ε不属于Vt(但是属于Vt)。做题经验:1. 在求FOLLOW(A)的时候,首先从开始符开始S推导,SεSε,所以 # FOLLOW(S);2.所有推导从S出发,必须想方设法使A的左边全属于Vt(左边不能有Vn,但是可以是ε),这一点是关键;3.特别注意ε和 #这两个符号;4. 求FOLLOW集的时候,A可能需要暂时消失,然后在后面再次出现;最开始就把VnVt列举出来,防止遗漏,防止把大小写看混

再引入第三个概念:SELECT(A)Aα是上下文无关文法的产生式。如果α无法最终推导出ε,则SELECT(Aα)=FIRST(α),如果α以”某终结符”开头,则FIRST(α)显然就直接是”某终结符”,如果是非终结符开头,则需要查询之前推导出的FISRT集。如果α能最终推导出ε,则SELECT(Aα)=(FIRST(α)ε)  FOLLOW(A)

定义有一点复杂,概括一下。关键点在于α能否推导出ε。如果不可以,那就很简单,只用考虑FIRST(α)集就可以了。如果可以推导出ε,那就麻烦一点,要还要加上FOLLOW(A)集。LL(1)的充要条件:对每一个非终结符A的两个产生式Aα,Aβ,满足SELECT(Aα)SELECT(Aβ)=,其中α,β不能同时推导出ε

LL(1)的特点是:无二义,不含左递归。每输入一个字符,可以确定使用哪一个产生式。假设输入a,现在需要推导A,那么a属于哪个SELECT(“A的某产生式”)集,就使用”A的某产生式”推导A。第一个L,是指从左至右扫描字符串;第二个L,是指分析过程中用最左推导,1表明向右看一个输入符号便可以决定推导使用那一个产生式(如果仅仅需要用FIRST集,向右看0个;如果需要用到FOLLOW集,才会向右看1个)。非LL(1)到LL(1)文法的转换方法有提取左公因子和消除左递归(包括直接和间接左递归)。不过,上面两种方法,并不能确保转换成LL(1)。

消除直接左递归的方法:假设有产生式1:SSa和产生式2:Sb。首先,文法推导出的句型的开头一定是式2中的b;其次,式1中的S是递归部分,需要把S变成新字符N,N构成新的右递归;最后,需要借助ε。记住上述三个要点,可以轻松写出改写后的文法。如果是间接左递归,则首先通过替换,变成直接左递归,然后消除直接左递归。


2 自下而上

自上而下的分析算法能力强,构造复杂,最常用和最有效。其核心是移进归约,需要用到stack。大体思想是:对输入符号串进行从左至右扫描,将输入字符逐个移进一个栈中,边移进边分析,一旦栈顶符号串形成某个句型的”句柄”(可归约串)时,而且对应”某产生式”的”右部”,就用”某产生式”的”左部”代替该”句柄”,这称为归约。重复上述过程,直到归约到栈中只剩下开始符时,则分析成功。自下而上分为两类:算符优先和LR分析,大题必考哟。自下向上分析的关键问题是如何确定句柄(可归约串),当栈顶已经形成了某句型的句柄时,就意味着可以归约了。

1 算符优先

首先提到 简单算符优先:它把非终结符和终结符的优先关系都求出来了,是一种规范归约。但是效率低,没多大实用价值,老师的PPT都没讲,肯定不考。算符优先分析方法:只考虑算符(广义为终结符)之间的优先关系。首先来看看三种优先关系,,。大写字母代表非终结符,小写字母代表终结符。

ab:仅有诸如Aab 或 AaBb的产生式
ab:仅有诸如AaB 的产生式,且 B+b或B+Cb
ab:仅有诸如ABb的产生式,且 B+a或B+aC

算符文法:设有一文法G,如果G中没有形如ABC的产生式,其中B、C为非终结符,则称G为算符文法(operater grammer)。简而言之,就是两个非终结符不能紧挨着。  算符优先文法:设有一个不含 ε 的算法文法G,如果对任意两个终结符a, b之间至多只有“大于,小于,等于”三种关系中的一种成立,则称G是一个算符优先文法(operator precedence grammar)。下面两个终结符的集合很重要。

FIRSTVT(B)={bB+ b  B+ Cb}
LASTVT(B) ={aB+ a B+aC}
做题总结:在求FIRSTVT(A)的时候,在产生式中找到左部是A的产生式开始推导;在推导时候,只关心最左边的符号,如果是终结符就结束该分支的推导,如果是非终结符就继续推导这个非终结符。同理,求LASTVT集的时候,只关心最右边的那个符号。记住可以“重复利用”前面推导过的字符。

构造算符优先关系表:(大写字母代表非终结符,小写字母代表终结符)

  1.   寻找诸如Aab 或 AaBb的产生式,那么ab
  2. 求出所有非终结符的FIRSTVT集和LASTVT集
  3.   寻找诸如AaB的产生式,记录所有的”aB”,在FIRSTVT(B)中找到所有对应的b,则ab,在表的左侧找到a,向右填写到表中
  4.   寻找诸如ABb的产生式,记录所有的”Bb”,在LASTVT(B)中找到所有对应的a,则ab,在表的顶上找b,向下填写到表中
  5. 有一些题目的产生式中没有’#’,但是最后的算符优先表中’#’不可缺少,记住补上

算符优先分析归约的关键,是如何找到最左素短语。归约的时候,非终结符是啥无所谓,可以把一个终结符变成任意一个“大写字母”。在栈的顶部的终结符a和当前输入的字符b之间比较有限关系,如果ab或者ab,则移进;如果ab则归约。在最终判断是否接受的时候,栈里面只有一个大写字母,然后剩余输入符号是空的,就行了。从这里可以看出,算符优先分析归约是不规范的,毕竟得不到真正的语法树。。。

素短语:设有一个文法G,素短语满足如下两个条件的短语,他至少包含一个终结符,并且除了自己以外,不包含其他素短语(有点绕,就是说它是一个“最小单位”)。最左素短语是一个句型中,最左边的素短语。


2 LR分析法

LR分析法是一种规范归约,能根据当前分析栈中的符号串和向右顺序查看k个(k0)符号就能唯一确定动作是移进还是利用那一个产生式归约,因而可以唯一确定句柄。LR(k)对文法的限制少,而且快、准、狠,不过缺点是分析器的构造工作量很大。LR(0)大题必考

最右推导是规范推导,最左归约是规范规约。在LR(0)归约的时候,有两个栈:状态栈和符号栈,有一个输入串,还有ACTION和GOTO。对于一个合法的句子,每次归约后得到的都是由“已归约部分”和”输入剩余部分”合起来构成文法的规范句型。每一次归约之前,规范句型的前部称为可归前缀(放在符号栈里面的就是这个东西)。”可归前缀的前缀”(可以包括可归前缀它自己)称为活前缀。活前缀更加直观的理解:规范句型的一个前缀,这种前缀不含句柄之后的任何符号。形式化定义如下:若 SRαAωRαβω (这里是双箭头) 是文法G的扩展文法G 的一个规范推导,αβ是前部(即可归前缀),符号串γαβ的前缀,则称γ是G的一个活前缀。在LR分析的过程中,实际上就是把αβ的前缀放在符号栈中,一旦符号栈中出现αβ,即句柄已经形成,则用产生式Aβ进行归约。

句柄:其定义前面已经给出了,然而句柄还有第二种理解方法。课本上有一句话:在规范规约的分析中,“可归约串”称作句柄。这和前面提到的“最左直接短语”的定义相差不小。最明显的差距在于,“最左直接短语”的定义方式决定了句柄只有一个(在一个句型中);而”可归约串“这种定义,暗示着句柄可以有很多个,一个文法有n个产生式,那么这n个产生式的右部可能都是句柄。LR分析方法是一种规范规约,那么这里的句柄就当作”可归约串“。课本上还有一句话:”可归约串“这个概念的不同定义,形成了不同的自下而上分析方法。这真是让人脑洞大开~

LC(A)={αSRαAω,αV,ωVT} 简而言之,LC(A)是在最右推导中在非终结符A左边出现的符号串的集合。这个是不包含句柄部分的串,如果再加上句柄加入上去,就得到了包含句柄的活前缀。对于LR(0)而言,只需要把LC(A)再加上产生式的右部就行了。LR(0)C(Aβ) = LC(A)β,这就是含句柄的活前缀,依据这个构造NFA,然后转换成DFA,这种方法是活前缀的一般计算方法。对于高级语言,这种方法实现起来很复杂。下面是构造归约前缀和活前缀的更实用的方法。

LR(0)项目集规范族,构成了识别一个文法的活前缀的DFA的状态的全体。这里引入了“”。一个产生式的右部,增加一个圆点 ,就变成了一个项目,圆点左部表示分析过程的某时刻欲用该产生式归约时,已识别过的句柄部分;圆点右部表示期待的后缀部分(当前还没有遇到,等待输入)。圆点用来指示位置,比如:Aβ,表明产生式 Aβ 的右部 β 已出现在了栈顶,句柄出现,可以归约了。Aβ1β2表明产生式 Aβ1β2 中的 β1 已经在栈顶,期待β2 的到来,β2 一旦到了之后,句柄就形成,可以归约了~

又有了两种方法:一种是求出文法的所有项目,构造一个识别活前缀的NFA,再把NFA转换成DFA;另一种是绕开NFA,直接构造DFA。后者具体如下:

  1. 把第一个项目作为初态集的核,对核求CLOSURE(核),得到初态的项目集
  2. 对初态集或其它构造的项目集应用转换函数GO( I, X ) = CLOSURE( J ),求出新的状态的项目集
  3. 重复步骤2直到没有产生新的项目。

这里的CLOSURE( J )集和GO( I, X )集分别和有限自动机里面的闭包运算和MOVE运算异曲同工:

  1. 状态 J 的项目均在CLOSURE( J )中
  2. 若 AαBβ 属于CLOSURE( J ),则每一形如B γ 的项目也在CLOSURE( J )中
  3. 重复步骤2,直到不产生新的项目集

GO( I, X ) = CLOSURE( J ),其中 I 为包含某一项目集的状态。X VnVt,J = {AαXβAαXβI}。具体操作而言,对状态 I 里面的项目遍历一遍,对每一个项目的””向右一个位置,得到新状态的核(就是 J),””经过的那个符号,就是该项目的X,再对新得到的核进行闭包操作,得到新的状态。

总共3种方法的出发点都是把LR分析法的归约过程,看成是识别文法规范句型的前缀的过程。只要分析到的当前状态(符号栈、状态栈)是活前缀的识别态,这说明已经识别过的部分是该文法的某一规范句型的一部分,也就是说已经分析过的部分是正确的。

LR(0)项目集规范族的项目类型有4种:移进项目Aααβ、归约项目Aα、待约项目AαBβ、接受项目SS 。存在两种冲突:移进—归约项冲突、归约—归约冲突。当一个文法的LR(0)项目集规范族没有上述两种冲突的时候,称此文法为LR(0)文法。

LR(0)分析表:是一个二维数组,行标是状态号,列标是文法符号和’#’,ACTION表和GOTO表重叠。查看ACTION表:先看行,假设是状态 x,再看列,假设符号是 y,而且是终结符,那么ACTION[x][y]的值就是移进、归约、接受、出错(空白)中的一个;y 如果是非终结符,GOTO[x][y]的值就是转向的状态。注意 y 是终结符(小写)或者非终结符(大写)。构造LR(0)分析表,首先要构造其识别活前缀的自动机DFA。至于如何把DFA转换成对应的LR(0)分析表,做一道题练练手就懂了。当在试图构造的LR(0)分析表时,没有出现多重定义,那么这样的分析表为LR(0)分析表,能构造LR(0)分析表的文法称为LR(0)文法。

LR(0)题目小结: 1.首先需要扩展文法,增加一个产生式; 2.状态编号从0开始,按照项目的顺序,向下依次标号,状态I1输入’#’的时候accept; 3.使用LR(0)分析表进行归约的时候,状态栈初始化为0,符号栈初始化为’#’; 4.归约,假设使用AαB归约,此时首先在状态栈中减去|αB|个状态,然后得到栈顶的状态假设是x,然后把GOTO(x, A)放入状态栈中。

SLR(1)分析法
LR(0)分析方法能力很弱,很多实用的程序设计语言的文法不能满足LR(0)文法。SLR(1)基于容许LR(0)规范族中有冲突的项目集(状态),用向前查看一个符号的办法来处理冲突。因为只对有冲突的状态才向前查看一个符号,以确定做那种动作,所以是Simple的。

SLR(1)出现移进-归约冲突的时候,需要求出归约项目的左部的FOLLOW集,这些FOLLOW集如果互不相交(可能有多个归约项目)并且和移进项目的圆点右边的终结符不相交,则符合SLR(1)文法。此时更改一下LR(0)分析表,就形成了新的SLR(1)分析表。


3 语义分析 中间代码 优化 及其他

在语法分析之后,便是语义分析。语义分析有两个功能:一,检查语法结构的静态语义(语法规则的良形式条件),即检查语法是合法的程序是否真正有意义,是一种程序的约束的描述,分为类型规则和作用域规则;二,动态语义(程序单元执行的操作)处理,执行真正的”翻译”,即生成中间代码。

很多编译程序采用属性文法对语义处理工作进行比较规范和抽象的描述。一个属性文法包含一个上下文无关文法和一系列语义规则,这些语义规则附加在文法的产生式上面。属性文法是一个三元组,A=( G, V, F ),G 是上下文无关文法,V 是属性的有穷集,F 是关于属性的断言或谓词的有穷集,每个断言与文法中的某个产生式相联。属性分成两类:继承属性和综合属性。区分规律:观察产生式 Aα的语义规则的”:=”的左半边,如果是 A 的属性,则是综合属性;如果是其他符号的属性,则是继承属性。

中间代码是源程序的一种内部表示方法,其复杂性介于源程序和目标程序。主要形式有4种:逆波兰式,四元式,三元式,树。中间代码可以进行优化,其宗旨是获得更好性能的代码。它对程序进行各种等价变换,使得变换过后的代码更高效。含有优化功能的编译程序,其优化是指对生成的目标代码进行优化,而不是编译程序本身得到优化,因此提高目标代码的效率,而不是编译程序的效率。其中局部代码优化是指局限与基本块内的优化。基本块是指程序中一顺序执行的语句序列,其中只有一个入口语句和一个出口语句。常见的代码优化技术有:

  • 删除多余运算。删除两个相同的表达式
  • 循环不变代码外提。减少局部变量的重复产生
  • 强度消弱。除法–>乘法,乘法–>加法, 加法–>位运算
  • 变换循环控制条件。减少循环次数
  • 合并已知量,变量传递。
  • 删除无用赋值

编译程序是一种常用的系统软件,与具体的机器有关,与具体的语言有关。编译程序是一个语言的翻译程序,是一种把源语言书写的程序翻译成目标语言的程序。编译过程一般分为前段和后端,前段包括词法、语法、语义、和中间代码生成;后端包括中间代码优化和目标代码生成。

2 0
原创粉丝点击