编译原理之绪论

编译原理之绪论

既然是说编译原理,那么就可以提问:

什么是编译器

编译器就是一个程序,它读入用某种语言编写的源程序,并翻译成一个与之等价的另一种语言编写的源程序。编译器身上还有一个任务,就是发现编译前和编译时源程序中的错误。
这里写图片描述
当目标程序是一个可执行的机器语言程序时,那么它就可以被用户调用,产生输出。
这里写图片描述
Java处理器结合了编译和解释过程,如下图所示。一个Java源程序首先被编译成一个称为字节码(bytecode)的中间表示形式。然后由一个虚拟机对得到的宇节码加以解释执行。这样安徘的好处之一是在一台机器上编译得到的宇节码可以在另一台机器上解将执行。通过网络躭可以完成机器之间的迁移。
为了更快地完成输入到输出的处理,有些被称为即时(just in time)编译器的Java编译器在运行中间程序处理输入的前一刻首先把字节码翻译成为机器码,然后再执行程序
这里写图片描述

举例 人工英汉翻译的例子:

In the room,he broke a window with a hammer
这个句子就是源语言 汉语就是目标语言 翻译即编译
第一步 分析源语言
1)语义分析
抓住句子的谓语动词,就抓住了文章的大概意思 broke(谓语) 表示打的意思
于是我们可以通过上下文获取一些信息:
比如 谁打,打谁,用什么打,打之后有什么效果等等
因为broke是主动语态,所以前文he主语就是施事者,a window 宾语就是就是受施者。当然如果是被动语态,前者则是受施者,后者是施事者。 with a hammer是补语,表示使用的工具。In the room 是状语,表示动作发生地点。
我们可以通过一个图来表示这个句子词语的关系。
这里写图片描述
这个图点对应着句子实体,边则对应着关系,图就可以当做中间代码,他不仅可以被翻译为汉语,还有日语,韩语,西班牙语,意大利语等等,中间是一种独立于具体的语言。
2)语法分析
要想进行语义分析,首先要划分句子成分。
主语和宾语一般是由名词短语构成,而状语补语一般是介词短语组成,所以要想识别句子成分,就得需要识别句子各类短语,这一过程就是语法分析。
3)词法分析
那怎么识别句子各类短语呢,根据词性。
比如一个冠词(a)加上一个名词(window),形成一个名词短语(a window);
一个冠词(the)加一个介词(in)再加一个名词(room),形成一个介词短语(in the room);

于是要想知道句子各类短语,则需要根据词性判断,这一过程为词法分析。

SO

这里写图片描述

编译器的结构

编译器能够把源程序映射为在语义上等价的目标程序,这个映射过程由两个部分组成:分析部分和综合部分。
分析(analysis)部分把源程序分解成为多个组成要素,并在这些要素之上加上语法结构。然后,它使用这个结构来创建该源程序的一个中间表示。如果分析部分检査出源程序没有按照正确的语法构成,或者语义上不一致,它就必须提供有用的信息,使得用户可以按此进行改正。分析部分还会收集有关源程序的信息,并把信息存放在一个称为符号表(symbol table)的数据结构 中。符号表将和中间表示形式一起传送给综合部分。
综合(symhesis)部分根据中间表示和符号表中的信息来构造用户期待的目标程序。分析部分经常被称为编译器的前端(front end),而综合部分称为后端(back end)。
这里写图片描述

词法分析

词法分析的主要任务就是从左向右逐行扫描源程序的字符,识别出各个单词,确定单词的类型。 将识别出的单词转换成统一的机内表示——词法单元(token)形式
< token-name,attribute-value >
token-name是一个由语法分析步骤使用的抽象符号。attribute-value指向符号表中关于这个词法单元的条目。符号表条目的信息会被语义和代码生成步骤使用。
词法单元将会被传送给下一个步骤——语法分析。
比如一个赋值语句:position = initial + rate * 60
这个赋值语句中的字符可以组合成下列词素,并映射成为如下词法单元。这些词法单元将被传递给语法分析阶段。
1)position 是一个词素,被映射成词法单元〈id, 1〉,其中id是表示标识(identifier)的 抽象符号,而1指向符号表中position对应的条目。一个标识符对应的符号表条目存放该标识符有关的信息,比如它的名字和类型。
2)赋值符号=是一个词素,被映射成词法单元〈=〉。因为这个词法单元不需要属性值,所以我们省略了第二个分量。也可以使用assign这样的抽象符号作为词法单元的名字,但是为了标记上的方便,我们选择使用词素本身作为抽象符号的名字。
3)initial 是一个词素,被映射成词法单元〈id, 2〉,其中2指向initial 对应的符号表条目。
4)+是一个词素,被映射成词法单元〈+〉。
5)rate是一个词素,被映射成词法单元〈id, 3〉,其中3指向rate对应的符号表条目。
6) * 是一个词素,被映射成词法单元〈 * 〉。
7)60是一个词素,被映射成词法单元〈60〉。
空格会被词法分析器忽略掉。上述赋值语句可以表示成以下词法单元序列:

程序设计语言大概可以分为五类词:

这里写图片描述
PS:program是C语言关键字,Java中没有
关键字是事先可以确定的,所以我们可以给每一个关键字提供一个 token-name,也就是一词一码。
标识符是一个数据对象和过程,是一个开放的集合,所以有可能token-name重合,多词一码,所以我们需要attribute-value来区分。
常量和标识符类似,是一个开放的集合,我们不能事先得到所有的常量,但是类型是固定的,所以可以为一型一码,区分可以用attribute-value
运算符包括算数/关系/逻辑运算符,以及界线符和关键字都是预先设定好的,也就是一词一码。

语法分析

语法分析器使用由词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示(从词法分析器输出的token序列中识别出各类短语,并构造语法分析树)。该中间表示给出了词法分析产生的词法单元流的语法结构。一个常用的表示方法是语法树(syntax tree),树中的每个内部结点表示一个运算,而该结点的子结点表示该运算的分量。
position = initial + rate * 60
这里写图片描述
这棵树显示了赋值语句中各个运算的执行顺序。这棵树有一个标号为 * 的内部结点,< id, 3 >是它的左子结点,整数60 是它的右子结点。结点< id, 3 >表示标识符rate。标号为*的结点指明了我们必须首先把rate 的值与60相乘。标号为+的结点表明我们必须把相乘的结果和initial的值相加。这棵树的根结点的标号为=,它表明我们必须把相加的结果存储到标识符position对应的位置上去。这个运算顺序和通常的算术规则相同,即乘法的优先级高于加法,因此乘法应该在加法之前计算。
这里写图片描述
D代表declaration首字母,表示声明语句。T代表Type,表示类型。IDS代表identify sequence,表示标识符序列。
编译器的后续步骤使用这个语法结构来帮助分析源程序,并生成目标程序。

语义分析

语义分析器(semantic analyzer)使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。它同时也收集类型信息,并把这些信息存放在语法树或符号表中,以便在随后的中间代码生成过程中使用。

收集标识符属性信息

这里写图片描述
这里写图片描述
这里写图片描述

语义检查

这里写图片描述
这里写图片描述
上图显示了一个这样的自动类型转换。假设position、initial和rate已被声明为浮点数类型,而词素60本身形成一个整数。上图中的语义分析器的类型检査程序发现运算符*被用于一个浮点数rate和一个整数60。在这种情况下,这个整数可以被转换成为一个浮点数。上图中,语义分析器输出中有一个关于运算符inttofloat的额外结点。inttofloat明确地把它的整数参数转换为一个浮点数。
语义分析的一个重要部分是类型检查(type checking)。编译器检查每个运算符是否具有匹配的运算分量。比如,很多程序设计语言的定义中要求一个数组的下标必须是整数。如果用一个浮点数作为数组下标,编译器就必须报告错误。
程序设计语言可能允许某些类型转换,这被称为自动类型转换(coercion)。比如,一个二元算术运算符可以应用于一对整数或者一对浮点数。如果这个运界符应用于一个浮点数和一个整数,那么编译器可以把该整数转换(或者说自动类型转换成为一个浮点数。

中间代码生成

在把一个源程序翻译成目标代码的过程中,一个编译器可能构造出一个或多个中间表示。这些中间表示可以有多种形式。语法树是一种中间表示形式,它们通常在语法分析和语义分析中使用。
在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。我们可以把这个表示看作是某个抽象机器的程序。该中间表示应该具有两个重要的性质:它应该易于生成,且能够被轻松地翻译为目标机器上的语言。
我们可以考虑一种称为三地址代码(three-address code)的中间表示形式。这种中间表示由一组类似于汇编语言的指令组成,每个指令具有三个运算分量。每个运算分量都像一个寄存器。上图中的中间代码生成器的输出是如下的三地址代码序列:
这里写图片描述
关于三地址指令,有几点是值得专门指出的。首先,每个三地址赋值指令的右部最多只有一个运算符。因此这些指令确定了运算完成的顺序。在源程序中,乘法应该在加法之前完成。 第二,编译器应该生成一个临时名字以存放一个三地址指令计算得到的值。第三,有些三地址指令的运算分量少于三个(比如上面的序列中的第一个和最后一个指令)。
这里写图片描述
PS:数组中i是偏移地址,不是下标
三地址指令可以使用四元式、三元式、间接三元式数据结构表示,以下为四元式举例。(op,y,z,x)op为操作符,y和z为元操作数,x为目标操作数。
这里写图片描述
下图中j表示跳转 jump 比如:100:当a < b,跳到102,否则继续执行 到101
这里写图片描述

代码优化

机器无关的代码优化步骤试图改进中间代码,以便生成更好的目标代码。“更好”通常意味着更快,但是也可能会有其他指标,如更短的或能耗更低的目标代码。比如,一个简单直接的算法会生成中间代码。它为由语义分析器得到的树形中间表示中的每个运算符都使用一个指令。
使用一个简单的中间代码生成算法,然后再进行代码优化步骤是生成优质目标代码的一个合理方法。优化器可以得出结论:把60从整数转换为浮点数的运算可以在编译时刻一劳永逸地完成。因此,用浮点数60.0来替代整数60就可以消除相应inttofloat运算。而且,t3仅被使用一次,用来把它的值传递给id1。因此,优化器可以把之前序列转换为更短的指令序列
t1 = id3 * 60.0
id1 = id2 + t1
不同的编译器所做的代码优化工作量相差很大。那些优化工作做得最多的编译器,即所谓的 “优化编译器”,会在优化阶段花相当多的时间。有些简单的优化方法可以极大地提高目标程序的运行效率而不会过多降低编译的速度。

代码生成

代码生成器以源程序的中间表示形式作为输人,并把它映射到目标语言。如果目标语言是机器代码,那么就必须为程序使用的每个变量选择寄存器或内存位罝。然后,中间指令被翻译成为能够完成相同任务的机器指令序列。代码生成的一个至关重要的方面是合理分配寄存器以存放变量的值。
比如,使用寄存器R1和R2,之前的中间代码可以被翻译成为如下的机器代码:
LDF R2, id3
MULF R2, R2, #60.0
LDF R1, id2
ADDF R1,R1, R2
STF id1, R1
每个指令的第一个运算分量指定了一个目标地址。各个指令中的F告诉我们它处理的是浮点数。代码中把地址id3中的内容加载到寄存器R2中,然后将其与浮点常数60.0相乘。井号“#”表示60.0应该作为一个立即数处理。第三个指令把id2移动到寄存器R1中,而第四个指令把前面计算得到并存放在R2中的值加到R1上。最后,在寄存器R1中的值被存放到id1的地 址中去。这样,这些代码正确地实现了最初的赋值语句。
上面对代码生成的讨论忽略了对源程序中的标识符进行存储分配的重要问题。我们将在以后谈到,运行时刻的存储组织方法依赖于被编译的语言。编译器在中间代码生成或代码生成阶段做出有关存储分配的决定。

符号表管理

编译器的重要功能之一是记录源程序中使用的变量的名字,并收集和每个名字的各种属性有关的信息。这些属性可以提供一个名字的存储分配、它的类型、作用域(即在程序的哪些地方可以使用这个名字的值〉等信息。对于过程名字,这些信息还包括:它的参数数量和类型、每个参数的传递方法(比如传值或传引用)以及返回类型。
符号表数据结构为每个变量名字创建了一个记录条目。记录的字段就是名字的各个属性。 这个数据结构应该允许编译器迅速查找到每个名字的记录,并向记录中快速存放和获取记录中的数据。

将多个步骤组合成趟

前面关于步骤的讨论讲的是一个编译器的逻辑组织方式。在一个特定的实现中,多个步骤的活动可以被组合成一趟(pass)。每趟读人一个输人文件并产生一个输出文件。比如,前端步骤中的词法分析、语法分析、语义分析,以及中间代码生成可以被组合在一起成为一趟。代码优化可以作为一个可选的趟。然后可以有一个为特定目标机生成代码的后端趟。
有些编译器集合是围绕一组精心设计的中间表示形式而创建的,这些中间表示形式使得我们可以把特定语言的前端和特定目标机的后端相结合。使用这些集合,我们可以把不同的前端和某个目标机的后端结合起来,为不同的源语言独立该目标机上的编译器。类似地,我们可以把一个前端和不同的目标机后端结合,建立针对不同目标机的编译器。

附总图

这里写图片描述

参考资料

《编译原理》
《哈工大编译原理视屏教程》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值