编译原理详细总结

编译原理

1 编译概述

  把高级程序语言翻译成汇编语言或机器语言的工作称为编译,完成这项翻译工作的软件系统称为编译程序或编译器。下图展示高级语言从编译到执行的大致过程(本文中图片来源均为张莉等编著的《编译技术》)。
编译原理详细总结_第1张图片
  对于纯编译型语言(如c),程序在执行之前必须要有编译过程。对于解释型的语言,可以对源代码或中间代码按行解释执行。一个更详细的过程如下图所示。
编译原理详细总结_第2张图片
  如果学过c语言就会对上图中提到的一些概念有更形象的理解:其中预处理器是在真正的编译开始之前由编译器调用的独立程序。预处理器可以删除注释、包含其他文件以及执行宏(宏macro是一段重复文字的简短描写)替代。源程序为.c文件,通过编译程序生成目标程序.o文件,经过汇编程序生成.obj文件。连接编辑程序把若干可重定位代码汇集连接起来,把分别编译的程序里的符号名转换成统一加载到存储器上的地址。这样,就可以处理用多种语言结合编写的程序。加载程序把连接在一起的机器代码以计算机可执行的形式加载到实际的存储器中。现在我们使用codeblocks等ide往往点击编译运行就直接从源程序生成了可执行文件。但是如果使用GCC命令行的话,可以对上述流程进行细化的执行。
  编译原理课程主要围绕如何构建一个编译程序(或编译系统)展开,其中一个典型的编译程序的架构如下图所示。
编译原理详细总结_第3张图片
  在这篇博文的后续部分将对编译程序中的词法分析、语法分析、符号表管理、存储机制和中间代码生成进行详细描述。

2 文法和语言的概念和表示

  文法和语言的概念和表示是编译原理后续内容的基础,什么是文法?简单地说,文法是对语言结构的定义和描述。什么是语言?用语言学的概念来解释,语言是由语音、语法和词汇三要素组成的人与人之间的一种交流方式,抛开语音不谈,我们可以认为一个语言L等于其组成句子的集合{S | S 符合 L的语法(或文法)}。那么语言三要素中的词汇在哪里体现呢?一方面我们可以非形式地说句子S中出现的符号都要属于这个语言的词汇。比较正规的说法是词汇构成了语言的非终结符集合,这一概念见下文中语言的形式化定义这一章节。
  程序语言是语言的一种,因此首先理解语言和文法,对于学习程序语言及其编译原理有着比较大的帮助。

2.1 文法的非形式化讨论

  当进行语言课程的学习的时候(不是指语文课或是英语课,而是Linguistic),在前三节课程之内必然会学习到语法树这个概念。“语法树”是句子结构的一种图形表示法。任何一个语法正确的句子都可以根据文法画出相应的语法树。一个语法树的示例如下:
编译原理详细总结_第4张图片
  语法树的非叶节点组成语言的非终结符,例如<句子>,<名词短语>(NP),<动词短语>(VP)等。语法树的叶节点为句子的终结符,例如单词和符号。要注意的是,语言具有二义性,一个合法句子对应的语法树可能不止一种,例如上述句子可以分解为(John,Called,(Mary from Denver))。语言的二义性造成了语法分析的困难,对于编程语言来说,是不允许一句编程语言出现二义性的。
  语言的语法结构也可以通过建立一组规则来描述(包括编程语言也可以),例如<主语>::=<冠词><形容词><名词>。在这种文法描述中,描述某个特定语法成分的规则可能不只一条。例如,上述规则中就有两条规则表示<名词>是由什么组成的。以后将会看到,这种情况的存在将给语法分析工作增加复杂性。
  当有了一组规则以后,就可以按照一定的方式用它们去推导或产生句子。例如

 - <句子><主语><谓语>
 - <主语><冠词><形容词><名词>
 - <谓语><动词><直接宾语>

  句子的推导存在多种方式,例如上式仅仅三条语法规则,就可以先拆分<主语>或先拆分谓语<谓语>。一个负责的语句,推导方式可能是难以枚举的。于是我们约定两种比较整洁的推导方式,分别为最左推导和最右推导(见下文介绍)。

2.2 文法的形式化定义

  要知道语言和文法的形式化定义。首先介绍符号、符号串及其集合的运算。其中字母表是元素的非空有穷集合。字母表中的元素称为“符号”。由字母表中的符号所组成的任何有穷序列称为“符号串”。允许有空符号串,用 ε \varepsilon ε表示。 ε \varepsilon ε是不包含任何符号的符号串。值得注意的是{ ε \varepsilon ε}并非空集合。
  符号串: 字母表 Σ \Sigma Σ上的符号串递归定义如下:

  1. ε \varepsilon ε Σ \Sigma Σ上的符号串;
  2. 若x是 Σ \Sigma Σ上的符号串,且 a ϵ Σ a\epsilon\Sigma aϵΣ,则ax和xa是 Σ \Sigma Σ上的符号串;
  3. y是 Σ \Sigma Σ上的符号串,当且仅当y可由(1)和(2)产生。

  符号串相等、符号串的长度在此不做详细讨论。
  符号串的联结: 若x和y是两个符号串,它们的联结记为xy,是将符号串y联结在符号串x的后面。一般来说,xy不等于yx。
  集合的乘积运算: 令A、B为符号串集合,两个符号串集合A和B的乘积AB定义如下:
A B = { x y ∣ x ϵ A , y ϵ B } AB=\{xy|x \epsilon A, y \epsilon B\} AB={xyxϵA,yϵB}
  符号串的幂运算: 符号串x的n次幂即n个x做联结运算。约定x的0次幂为 ε \varepsilon ε
  集合的幂运算: 符号串A的n次幂即n个A做乘积运算。约定A的0次幂为{ ε \varepsilon ε}。
  集合的闭包 A ∗ A^* A与正闭包 A + A^+ A+ 用文字描述来说,集合的闭包就是集合的所有n次幂中的符号串组成的集合,公式定义如下。
A + = A 1 ∪ A 2 . . . ∪ A n ∪ . . . A ∗ = A + ∪ A 0 A^+=A^1\cup A^2...\cup A^n\cup ... \\ A^*=A^+\cup A^0 A+=A1A2...An...A=A+A0
  文法: 文法可被定义为一个四元组,即 G = < V n , V t , P , Z > G = G=<Vn,Vt,P,Z>。其中 V n V_n Vn为非终结符集合, V t V_t Vt为终结符集合,P为规则(或产生式)的集合,Z为文法的识别符号。
  产生式或规则: 产生式或重写规则(简称规则)是有序对(U, x),其中U是一个符号(长度为1),x是一个长度可为0且有穷的符号串。有序对(U, x)表达如下:
U : : = x 或 U → x , ∣ U ∣ = 1 且 ∣ x ∣ ≥ 0 U::=x 或U\to x, |U|=1 且 |x|\ge 0 U::=xUxU=1x0
  字汇表: 在所有的规则中,规则左部和右部中的所有符号组成一个集合,称为文法的字汇表,记为V。字汇表进一步分为终结符集和非终结符集。
  终结符号与非终结符号: 给定文法G,我们把作为规则左部出现的那些符号称为非终结符号(注意左部分长度为1,所以非终结符号不能为一个符号串)。它们形成了非终结符号集,记为 V n V_n Vn。而规则中不属于Vn的那些符号称为终结符号,它们形成了终结符号集合,记为 V t V_t Vt
  识别符号: Z是一个符号,它至少要在一条规则中作为左部出现,通常把这个符号称为“识别符号”(开始符号)。

2.3 推导的形式化定义

  推导可分为直接推导和间接推导,所谓推导和规则的区别就是,推导是根据规则,生成句子的一个过程。推导描述了符号串和符号串的关系,也就是说,对于符号串v和符号串w, v ⇒ w v\Rightarrow w vw指通过一条产生式或规则,替换v中的一个非终结符U,可以使v和w相等,即v直接推导出w。直接推导和间接推导的形式化定义如下:
  直接推导: 设有文法G=,V= Vn∪Vt。如果存在v∈V+, w∈V*,且v=xUy,w=xuy,其中,x、y ∈V* ,U∈Vn , u ∈V*,若存在U ::= u∈P,则v ⇒w。
  间接推导: 多个直接推导。

2.4 语言的形式化定义

  设G[Z]是定义在字汇表V上的一个文法,文法G[Z]产生的所有句子的集合,记为L(G[Z]),称为文法G[Z]所定义的语言。公式定义如下:
L ( G [ Z ] ) = { x ∣ x ϵ V t ∗ , 且 Z 间 接 推 导 x } L(G[Z])=\{ x|x \epsilon V_t^*,且Z间接推导x\} L(G[Z])={xxϵVt,Zx}
  其中句型、句子的定义如下:
  句型: Z间接推导出x,且x属于V*,则称x是文法G的一个句型。例如<动词短语>是一个句型。
  句子: Z直接或间接推导出x,且x属于Vt*,则称x是文法G的一个句子(句子是语言的最小单位)。

2.5 推导与规约、短语、句柄与语法树

  这一部分的讨论见我的另一篇博文编译原理之推导、规约、句柄与文法的二义性。

2.6 文法和语言分类

  这一部分网上资料较多,以下引用一下较好的资源。文法和语言分类

3 词法分析

  词法分析程序的功能是:扫描源程序字符,按语言的词法规则识别出各类单词符号(Token)(简称单词或符号),并将有关字符组合成为单词并输出,同时进行词法检查。归纳起来词法分析器所要做的工作有:

  1. 从源程序的第一个字符开始,顺序地读字符,一次读一个,根据所读进的字符识别各类单词。有时还需要超前读几个字符才能确定是什么单词符号。当确定单词类别以后,就可根据词法规则将有关字符组合成为单词,并将其输出。在组合单词过程中同时进行词法检查。若发现单词的组成有错误时,应输出有关的出错信息。
  2. 对数字常数完成数字字符串到十进制数值的转换。并将其值输出。
  3. 删去空格、换行、制表等字符和注释。

  下文主要从单词符号、自动机与状态图、词法分析器伪代码三方面顺序地介绍词法分析过程。

3.1 单词符号

  单词是语言的基本语法单位,具有确定的语法意义。通常程序语言的单词符号包括:

  1. 保留字:指程序设计语言预定义的字符串,如begin,end,for,if,then,do等等。它们在语言中具有固定的意义,是编译程序识别各类语法成分的依据,所以用户不能把它作为标识符使用。
  2. 标识符:由用户定义用来表示各种名字的字符串,如变量名、数组名和过程名等。
  3. 常数:通常包括无符号数、布尔常数、字符串常数等,如:125,0.718,15.2,0x58A,‘%’,TURE,“table_list”等。
  4. 分界符或操作符:可分为单字符分界符如+、-、、/、,、;、(、 )、:等以及双字符分界符如//、/、:=等。

3.2 自动机与状态图

  计算机控制系统的控制程序具有有限状态自动机(FA)的特征,可以用有限状态机理论来描述。有限自动机(Finite Automata Machine)是计算机科学的重要基石,它在软件开发领域内通常被称作有限状态机(Finite State Machine),是一种应用非常广泛的软件设计模式。有限自动机的详细内容见本人博文计算理论。
  状态图是有限状态机的只直观表示。正则文法的分析就可以使用状态图进行。
编译原理详细总结_第5张图片

3.3 词法分析器

   这一部分网上资料较多,以下引用一下较好的资源。
   词法分析器算法流程(引用《编译原理教材》):
编译原理详细总结_第6张图片

  词法分析器代码实现:链接

4 语法分析

  语法分析是编译过程的核心部分。语法分析的任务是:按照文法,从源程序符号串中识别出各类语法成分,同时进行语法检查,为语义分析和代码生成做准备。执行语法分析任务的程序称为语法分析程序,也称为语法分析器,它是编译程序的主要部分之一。

4.1 自顶向下分析方法

  带回溯的自顶向下分析方法: 这是自顶向下分析的一般方法,即对任一输入符号串,试图用一切可能的办法,从树根结点(识别符号,又称开始符号)出发,根据文法自上而下地为输入符号串建立起一棵语法树;或者说,从识别符号开始,根据文法为输入串建立一个推导序列,这种分析过程本质上是一种试探过程,是反复使用不同规则谋求匹配输入串的过程。根据以上分析,不难编写程序来实现这种分析算法。但是,上述这种自顶向下的分析算法存在着一定的困难和缺点。1. 不能为左递归文法构造自顶向下的语法分析器,因为采用自顶向下的分析算法处理左递归文法必然会导致死循环。2. 存在着回溯问题。回溯必将影响效率,尤其是更加复杂的文法和句子,回溯的量会非常大。
  左递归解决办法: 左递归问题指文法中存在形如U ⇒ \Rightarrow U…的直接推导或间接推导。左递归将导致自顶向下分析方法进入死循环。消除文法中的左递归问题可以采用三条规则(用于解决直接左递归),和一个不常用的算法(用于解决间接左递归)。规则1(提因子) U::=xy|xw|…|xz可替换为U::=x (y|w|…|z)。规则2 U::=x|y|…|z|Uv可替换为U::=(x|y|…|z){v},相当于消除了产生式右部的左递归项。规则3 若有规则:P ::= Pa|b,则可改写为:P ::=bP’和P’::=aP’| ε。
  回溯解决办法: 当文法右侧存在多个候选式的时候,算法会逐一试探、回溯,效率极低,代价极高。为了避免回溯,就必须保证:对文法的任何非终结符号,特别是对那些规则右部有多个选择(候选式)的非终结符号,当要用它去匹配输入串时,它能够根据所面临的输入符号准确地用其规则右部的某一个选择去执行任务,并且工作的结果应是确信无疑的。为了找到满足要求的文法,我们定义任一个选择 α \alpha α的所可能推出的终结符号串的首符号集First集合如下:
F i r s t ( α ) = { s ∣ α ⇒ s A , 其 中 s 为 终 结 符 , A 为 可 为 空 的 符 号 串 } First(\alpha)=\{s|\alpha \Rightarrow sA,其中s为终结符,A为可为空的符号串\} First(α)={sαsAsA}
  下文LL(1)分析方法中会具体介绍给定任意文法,其各个非终结符号的First集合求法。对文法中的任意一个非终结符号,其规则右部有多个选择时,由各个选择所推出的终结符号串的头符号集合要两两不相交。这样,对于产生式右部的多个选择,算法可以通过first集合进行唯一的选择。
  递归下降分析法: 递归下降分析法的分析过程是按文法规则自顶向下一级一级地分配任务,即调用有关的子程序来完成。对文法中每个非终结符号U(它们都分别代表一种语法成分)都编出一个子程序,以完成该非终结符号所对应的语法成分的分析和识别任务。为什么针对某些非终结符号所编出的分析程序要编成递归子程序(递归下降分析法)?这主要是方便处理文法中出现的各种递归,如右递归性。
  递归下降分析的语法分析程序构造: 递归子程序法即递归下降分析的程序实现,递归子程序法与那些能使用部分自动化系统的方法(后面介绍)相比,其主要缺点是需要做更多的手工编写程序和调试程序的工作。但是,由于有上述那些优点,所以,该方法至今仍然被认为是一种合理的方法,并为很多编译程序所采用。例如,对于文法:
Z ∷ = ‘ ( ’ U ‘ ) ’ ∣ a U b U ∷ = ( d Z ∣ e ) { d } Z∷=‘(’U‘)’|aUb \\ U∷=(dZ|e)\{d\} Z::=(U)aUbU::=(dZe){d}
  可设计子程序Z和U分析该文法(来自北航计算机学院编译课程课件):
编译原理详细总结_第7张图片
编译原理详细总结_第8张图片

4.2 LL(1)分析方法

  LL(1)分析文法指相应的语法分析将按自左向右扫描输入字符串,并产生最左推导。其中,对于给定文法,先构建First、Follow集合,并给出分析表,详见博文。其中First集合和Follow集合的简单理解如下:

  • First集合: 对于某个非终结符A的first集(first(A)),简单地说就是由A推导得到的串的首符号的
    集合:A->aB,那么这里的a就属于first(A)。
  • Follow集合: 则是紧随A的终结符号集合,例如B->Aa,这里的a就属于follow(A)。

  LL(1)分析表的构建: 假定每个非终结符的Follow集合和每个产生式候选式的First集合都已经求出,那么对于文法中的每一个规则 A → α A\to \alpha Aα按如下规则确定分析表中的每一个元素:

  1. 对First( α \alpha α)中的每一个终结符a,置分析表[A, a]为 A → α A\to \alpha Aα
  2. ε ϵ F i r s t ( α ) \varepsilon \epsilon First(\alpha) εϵFirst(α),则对于属于Follow(A)的每一个终结符b或#,置分析表[A, b]为 A → ε A\to \varepsilon Aε。(由此可见,分析表可能产生冲突)
  3. 分析表中空的部分为error

  基于LL(1)分析表的分析过程: 分析开始时,将#S加入符号栈,将文本指针指向符号串的开始位置。之后不断循环:

  1. 符号栈栈顶符号为Xm,输入符号串指向字符 x i x_i xi
  2. 若Xm为#,则分析结束
  3. 若Xm等于 x i x_i xi,表明栈顶符号与当前字符匹配,将当前符号出栈并将文本指针向后移动。
  4. 若Xm为非终结符,则根据分析表[Xm, x i x_i xi]查表。

4.3 自底向上分析方法

4.4 LR语法分析方法

  LR分析的基本原理是自左向右扫描输入字符串,当发现当前规范句型的活前缀中出现句柄时就进行规约,直到最后规约为文法的识别符号。所以LR分析的关键是要有一个能识别规范句型活前缀的有穷自动机。
  LR语法分析器由输入符号串、符号栈、控制程序,以及包含动作(Action)和转移(Goto)两部分的LR分析表。分析程序每次从缓冲区读入一个符号,使用S1等状态符号(代表自动机状态)和xi等文法符号存储在符号栈中。根据当前栈顶状态符号和当前输入符号来根据LR分析表决定移进还是规约。
  移进:把当前符号移进符号栈,并根据Goto(S, a)进行状态转移。
  规约:按照产生式将句柄规约为非终结符。

5 符号表管理

  符号表是在编译过程中,编译程序用来记录源程序中各种名字(即标识符)的特性信息的表格。根据前文介绍,一个标识符在源程序中的每一次出现都需要与符号表打一次交道。符号表的管理(插入和查询)联系到编译程序执行的全过程。
  在符号表上最常执行的操作是插入(登录符号表)和查表(也称检索),这些操作根据所编译的语言是否具有显式声明而稍有不同。对于分程序结构的语言(后面将进行介绍),在建立和删除符号表时还要使用另外两种操作,称为定位和重定位(Set and Reset)。当在编译过程中识别出是分程序的开始时,就执行定位操作;当遇到分程序结束时,就执行重定位操作。
  符号表存储程序中出现的标识符及其一系列属性。下面所列的每一项特性信息并不是对所有编译程序都必须的;但对一个具体的编译程序的实现来说,对每一项都应当给予考虑。

  1. 名字(标识符)的种类:如简单变量、函数、过程、数组、标号、参数等
  2. 类型:如整型、实型、字符型、指针等
  3. 地址:标识符所分配单元的首地址或地址位移
  4. 作用域的嵌套层次:分程序结构语言中,标识符声明所在的分程序的层次
  5. ……

5.1 非分程序结构语言的符号表组织

  本节所指的非分程序结构语言是这样一种语言,用它来编写的每一个可独立进行编译的程序单元是一个不包含子模块的单一模块,在该模块中声明的所有标识符是属于整个模块的。
  在非分程序结构语言中,标识符具有全局和局部两种作用域。子程序名和函数名以及公共区名属于全局量,程序中每个部分都能引用。程序单元中说明的变量是局部量,仅在该单元内部有效。所以在这类语言的处理中,将全局符号表和局部符号表分开进行管理,其基本处理方法为:

  1. 将子程序、函数名和公共区变量填入全局符号表。
  2. 在程序单元的声明部分读到标识符时,构造局部符号表。
  3. 在程序的可执行语句部分读到标识符时,需要查表以判断该标识符是否已经声明。
  4. 当一个程序单元编译结束后,如果是一遍扫描的编译程序,该程序单元的局部符号表不再需要,应将其释放。

  符号表的组织方式可分为无序符号表、有序符号表和散列表三种,其中无序符号表插入、管理简单,但是每一次查询的代价较大。有序符号表通过字典顺序对标识符进行排序,插入代价较大,每一次查询代价较小。散列表基于散列表技术进行存储,消耗固定的存储空间,每一次查询的代价最小。

5.2 分程序结构语言的符号表组织

  分程序结构语言是指用这种语言所写的模块可包含嵌套的子模块,同时每一个子模块可以拥有属于自己的一组标识符,如局部变量,函数参数等。在模块A中所声明的标识符在整个模块A中都能访问,除非同样的标识符在A的子模块中又被重新声明,重新声明的标识符只在所声明的子模块中可用。
  在分程序结构语言中,标识符的作用域局限在所定义的最小模块中。在这类语言的符号表管理中,建表和查表均要遵循标识符的作用域规定进行,其基本处理方法为:

  1. 在声明部分读到标识符时,首先要查一下该标识符所在程序单元的符号表,如果表中已经存在同名的标识符,则说明重复声明了标识符,应该报错。如果表中不存在同名标识符,则将新声明的标识符插入表中。
  2. 在程序的可执行语句部分读到标识符时,需要查表以判断该标识符是否已经声明:首先查标识符所在程序单元的符号表,如果表中存在同名的标识符,则说明该标识符已声明;如果在本层符号表中未查到同名的标识符,则再查直接外层的符号表,如果在逐层向外查找的过程中找到同名标识符,说明该标识符已经声明,则读取该标识符信息供使用;如果到最外层仍未找到,说明使用了一个未声明的标识符。
  3. 对于语言预定义的一些过程或函数名字等标识符的子集,用户不必声明即可全程使用,设计编译程序时,标准名字及其数目都是已知的,所以可以将它们放在最外层的符号表中。
  4. 当在编译过程中识别出是分程序的开始时,执行定位操作;当遇到分程序结束时,执行重定位操作。

  定位和重定位操作指:当编译进入一个分程序时,通过定位操作为新声明的标识符建立一个子表。新声明的标识符的属性随着不断的分析和了解被填入符号表中,但当编译到该分程序的结尾时就可将它们从符号表中删除。对于一个分程序,实际删除其在表中的登记项是由重定位操作来实现的。。在编译到分程序的结束处完成重定位操作,使符号表恢复到进入该分程序以前的情况。
  分程序结构语言的符号表组织方式分为栈式符号表和散列符号表的栈式实现。其中栈式符号表的核心思路是:当遇到标识符声明时就将包含有标识符属性的记录推入堆栈。当到达分程序结尾时,就将在这个分程序中声明的所有标识符的记录移出堆栈。在栈式符号表的定位操作中,在分程序索引表的顶端将产生一个新的分程序索引项,并赋予当前符号表栈栈顶的地址。由此建立起一个指向当前分程序的第一个记录单元的指针。重定位操作将有效地清除刚刚被编译完的分程序在栈式符号表中的所有记录。通过把分程序索引表的顶端元素的值填入到TOP中来完成重定位操作,TOP指示符号表顶端第一个空闲存储单元。例如,对于如下程序的分程序索引符号表如下:
编译原理详细总结_第9张图片
编译原理详细总结_第10张图片

6 运行时的存储组织及管理

6.1 静态存储分配

  对于静态存储分配,就和上文符号表的思路一样,例如,对于c语言,声明固定大小的变量或常量之后,静态存储分配会在符号表中计算好每一个变量的地址(相对)。当程序被加载到内存中之后,操作系统可以根据这个固定的地址找到每一个变量。

6.2 动态存储分配

  活动记录:程序内每个程序模块都有自己的数据区,在程序运行中模块被调用时,可以从总的数据区请求一块空间,并保留该空间直到模块执行完成。这个数据区一般被称为活动记录(Activation Record),一个活动记录开始地址被称为活动记录的基,通过ABP指针指向这个地址。一个典型的活动记录包含局部数据区、参数区和display区。
  局部数据区:用于存储各变量等数据。
  参数区:参数区用于保存隐式和显式参数。隐式参数可以包含一个返回地址、指向前一个活动记录基的指针(previous active record base pointer,prevabp)和一个返回值。。当被调用模块执行完时,程序控制返回到调用程序中调用语句的下一条可执行语句,该语句的地址称为返回地址,程序从该点开始又继续往下运行。参数区的另一部分是显式参数区(有时称为过程调用的变元区)。
  display区:虽然在C、C++、Java等语言中已经不再需要利用display区访问外层变量,因为它们对嵌套程序之间的相互调用做了更严格的限制甚至禁止,但是在PASCAL等语言中,display区是一种巧妙快速访问外部变量的重要机制。为了访问到对当前模块来说是全局变量的每个变量,需要在display区保留一些必要的信息,它由一系列指针组成,每一个指针指向一个模块的活动记录的开始位置,而这个模块对于当前正在执行的模块来说是全局的。如ABP1,ABP2,可能指向当前模块外层的大模块。
编译原理详细总结_第11张图片
  运行时的地址计算:给定一个要访问的具有地址为(BL,ON)的变量,设该变量是在LEV层的一个模块中。该变量的地址ADDR可按如下方法计算:

if ( BL == LEV )
ADDR =abp+(BL - 1)+nip+ON;
else if ( BL<LEV )
ADDR = display[BL] + ( BL-1 ) + nip + ON;
else
printf(“地址错,不合法的模块层次”);

  其中,(BL - 1)+nip指当前模块中,在目标变量之前的变量和参数大小,ABP是活动模块地址。而如果目标变量在当前模块的外层,则通过display[BL] 访问活动记录中的第BL个元素。
  递归过程的处理:对于递归分程序结构的语言来说,一个程序模块可以和多个活动记录相关。每调用一次递归函数就会在运行栈加入新的活动记录,返回地址也与每个活动记录相联系,并留下一个空间用于保存函数的返回值。

7 源程序的中间形式

8 语义分析和代码生成

8.1 语法与语义的讨论

  从语言学的角度看:

Grammar: the rules in a language for changing the form of words and joining them into sentences.
Semantics: the meaning of words, phrases or systems.

  语法是句子构成的规则,语义则是词义、句义。在自然语言处理的任务中,词法与语法的分析被归类为Syntax,语义、句义等被归类为Semantics(分类来源:ACL2020)。我们(机器)是如何理解自然语言的语法的呢?可以参见本人的博文POS与依存句法。自然语言是不精确且模糊的,对于Syntax方向的任务,已有工作构建了各种精确和模糊的算法,如语法规则、HMM等,仍无法准确的解决分词、词性标注、句法分析等任务。语法分析看起来已经能够让我们了解一句话的构成了,我们还缺什么呢?试想构造一个问答系统,系统仅根据Syntax层级的方法,对用户输入的问题进行解答。例如,句子库中有,北航是一所大学(什么 是 什么),那么用户提问到北航是什么的时候,就会仅从语法的角度,得到“大学”的答案。但我们是不是忽略了什么?1. 句子库中的句子可能用不同的句子形式描述信息:如“作为一所大学,北航……”,“北京的大学有:……北航……”,“……类似的大学,如北航……”。语法层的分析能否穷尽这些形式。2. 句子库中的句子可能是非法的。3. 我们怎么知道用户的问题是在问什么。4. 如果问“北航是一所学校吗”,机器很可能会回答否。
  由此可见,仅针对自然语言,语法分析是不足以“理解”语言的。必须进一步对自然语言进行语义层面的分析,这一部分见我的博文语言模型。
  再回到程序语言,前文中我们讨论了编译器的词法分析、语法分析,以及符号表和存储管理。词法分析让程序能够从输入的符号串中提取出一个个Token,语法分析将Token规约为语法成分。对于程序语言,如果分析完语法,能不能直接用去生成中间代码,不需要语义分析的环节?在编译原理中,语义分析的作用是可以检查语法正确的语句是否存在语义问题,比如所有编译器都会做的赋值语句的类型检查。语义分析的另一个作用是分析得到相应的语义动作。语义动作如表达式求值符号表填写中间代码生成等。因此,在编译过程中,从语法分析到中间代码生成之间必然要有语义分析的环节。

8.2 语法制导翻译技术

8.2.1 翻译文法

  翻译文法将产生式所包含的语义,用一个或多个子程序(称为语义子程序或语义动作)来描述,并将这些子程序名插入到产生式中的相应位置。在语法分析过程中,当使用该产生式时就可在合适的时间调用这些子程序完成所需的翻译。
  例如,我们要设计一个计算器程序,输入为中缀表达式,要打印输出等价的后缀表达式。假定输入序列是:a+bc,对应的语义动作是READ(a) PRINT(a) READ(+) READ(b) PRINT(b) READ(*) READ(c) PRINT(c) PRINT(*) PRINT(+)。在该序列中,若用输入符号本身表示读操作,以符号“@”开始的符号串“@x”表示输出符号x的操作,于是上述语义动作序列可改写为:a@a+b@bc@c@*@+。这个序列被我们称之为一个“活动序列”。我们称符号“@”为动作符号标记,由符号“@”开始的符号串称为一个动作符号。(注意动作符号的本质是一个语义动作或语义子程序,不光是打印功能)
  将上文的语义动作符号加入到文法中,即读入特定的符号序列之后执行特定的语义动作。于是得到中缀表达式转换为后缀表达式的翻译文法如下:
编译原理详细总结_第12张图片
  综上可得到如下定义:翻译文法是上下文无关文法。在这个文法中,终结符号集由输人符号和动作符号组成。由翻译文法确定的语言中的符号串称为活动序列。

8.2.2 语法制导翻译

  所谓语法制导翻译,就是给定一输入符号串,根据输入文法获得翻译该符号串的动作序列,并执行该动作序列所规定的动作的过程。因此,给定一个翻译文法(定义了一个活动序列集),通过将每个活动序列的输入序列与动作序列配对可得到对偶的集合。这种对偶集合称为由给定翻译文法所定义的翻译。
  翻译文法确定了进行符号串翻译的动作序列,而翻译文法则是由输入语言的文法在产生式右部的适当位置插入动作符号形成的。因此,翻译的动作序列是受输入语言的文法控制的。采用语法制导翻译的方法来实现语言的翻译,就要根据输入语言的文法,分析各条产生式的语义,确定要求计算机完成的操作,分别编出完成这些操作的子程序或程序段(称为语义子程序或语义动作),并把这些子程序或程序段的名字作为动作符号插入到输入文法各产生式右部的恰当位置,从而形成翻译文法。

8.2.3 属性翻译文法

  翻译文法中的符号,包括非终结符、终结符和动作符号都是有穷集合中的符号,都没有值的概念。现在我们扩充翻译文法的概念,即使符号包含有值部分。符号所具有的值部分,我们称之为该符号的属性。考虑属性的翻译文法即为属性翻译文法。

8.2.4 自顶向下语法制导翻译

8.3 不同语句的语义分析及代码生成

你可能感兴趣的:(基础知识,编译器,编程语言,软件工程师)