编译原理-第二章 一个简单的语法制导翻译器

1. 引言

 编译器的分析阶段把源程序生成中间代码,在合成阶段把这个中间代码翻译成目标代码。

程序设计语言的语法(syntax)描述了该语言的程序的正确形式,而该语言的语义(semantics)则定义了程序的含义。

给出一个广泛使用的表示方法来描述语法,即上下文无关文法或BNF(Backus-Naur范式)。

上下文无关文法还可以指导程序的翻译过程。面向文法的编译技术,即语法制导翻译(syntax-directed translation)技术。

编译器前端模型:

标识符由多个字符组成,在语法分析阶段被当作一个单元进行处理。这样的单元称作词法单元(token)。

有两种中间代码。一种称为抽象语法树(abstract syntax tree),或简称为语法树(syntax tree)。另一种中间表示形式,它是一组三地址序列。x = y op z。三地址指令最多执行一个运算,通常是计算、比较或者分支跳转运算。

语法树:

 

 三地址代码:

2. 语法定义

文法自然地描述了大多数程序设计语言的层次化语法结构。

if语句的构造规则:

  stmt -> if ( expr ) stmt else stmt

这样的规则称为产生式(production)。if 和括号这样的词法元素被称为终结符号(terminal)。像expr这样的变量表示终结符号序列,被称为非终结符号(nonterminal)。

2.1 文法定义

 一个上下文无关文法(context-free grammar)由四个元素组成:

1. 一个终结符号集合,它们有时也成为“词法单元”。

2. 一个非终结符号集合,它们有时也成为“语法变量”。每个非终结符号表示一个终结符号串的集合。

3. 一个产生式集合,其中每个产生式包括一个称为产生式头或左部的非终结符号,一个箭头,和一个称为产生式体或右部的由终结符号级非终结符号组成的序列。

4. 指定一个非终结符号为开始符号.。

示例:

list -> list + digit

list -> list - digit

list-> digit

digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

如果某个非终结符号是某个产生式的头部,我们就说该产生式是该非终结符号的产生式。一个终结符号串是由零个或多个终结符号组成的序列。零个终结符号组成的串称为空串(empty string),记作e。

2.2 推导

根据文法推导符号串时,我们首先从开始符号出发,不断将某个非终结符号替换为该非终结符号的某个产生式体。可以从开始符号推导得到的所有终结符号串的集合称为该文法定义的语言(language)。

语法分析(parsing)的任务是:接受一个终结符号串作为输入,找出从文法开始符号推导出这个串的方法。

2.3 语法分析树

 给定一个上下文无关文法,该文法的一棵语法分析树(parse tree)是具有以下性质的树:

1)根节点的标号为文法的开始符号。

2)每个叶子节点的标号为一个终结符号或e。

3)每个内部节点的标号为一个非终结符号。

4)如果非终结符号A是某个内部节点的标号,并且它的子节点的标号从左至右分别为X1,X2,...,Xn,那么必然存在产生式A->X1X2...Xn。

一棵语法分析树的叶子节点从左向右构成了树的结果(yield),也就是从这棵语法分析树的根节点上的非终结符号推导得到(或者说生成)的符号串。

一个文法的语言的另一个定义是指任何能够由某棵语法分析树生成的符号串的集合。为一个给定的终结符号串构建一棵语法分析树的过程称为对该符号串进行语法分析。

2.4 二义性

一个文法可能有多棵语法分析树能够生成同一个给东的终结符号串。这样的文法称为具有二义性(ambiguous)。

2.5 运算符的结合性

当一个运算分量的左右两侧都有运算符时,我们需要一些规则来决定哪个运算符被应用于该运算分量。运算符“+”是左结合的,因为当一个运算分量左右两侧都有“+”号时,它属于左边的运算符。

2.6 运算符的优先级

当多种运算符出现时,我们需要给出一些规则来定义运算符之间的相对优先关系。

image

 具有n层优先级的情况,需要n+1个非终结符号。

通常这个非终结符号的产生式体只能是单个运算分量或括号括起来的表达式。然后,对于每个优先级都有一个非终结符,表示能被该优先级或更高优先级的运算符分开的表达式。通常,这个非终结符的产生式有一些产生式体表示了该优先级的运算符的应用;另有一个产生式体只包含了代表更高一层优先级的非终结符号。

3. 语法制导翻译

语法制导翻译是通过向一个文法的产生式附加一些规则或程序片段而得到的。

语法制导翻译相关的概念:

1. 属性(attribute):属性表示与某个程序构造相关的任意的量。比如:数据类型、指令数目等等。

2. (语法制导的)翻译方案(translation scheme):翻译方案是一种将程序片段附加到一个文法的各个产生式上的表示法。当在语法分析过程中使用一个产生式时,相应的程序片段就会执行。

3.1 后缀表示

一个表达式E的后缀表示(postfix notation)可以按下面的方式进行归纳定义:

1)如果E是一个变量或常量,则E的后缀表示是E本身。

2)如果E是一个形如E1 op E2 的表达式,其中op是一个二目运算符,那么E的后缀表示是E1' E2' op,这里E1'和E2'分别是E1和E2的后缀表示

3)如果E是一个形如(E1)的被括号括起来的表达式,则E的后缀表示就是E1的后缀表示。

3.2 综合属性

 语法制导定义(syntax-directed definition)把每个文法符号和一个属性集合相关联,并且把每个产生式和一组语义规则(semantic rule)相关联,这些规则用于计算与该产生式中符号相关联的属性值。

如果某个属性在语法分析树节点N上的值是由N的子节点以及N本身的属性值确定的,那么这个属性就称为综合属性(synthesized attribute)。

image

3.3 简单语法制导定义

要得到代表产生式头部的非终结符号的翻译结果字符串,只需要将产生式体中的各非终结符号的翻译结果按照它们在非终结符号中的出现顺序连接起来,并在其中穿插一些附加的串即可。具有这个性质的语法制导定义称为简单(simple)语法制导定义。

3.4 树的遍历

一个树的遍历(traversal)从根节点开始,并按照某个顺序访问树的各个节点。

一次深度优先(depth-first)遍历从根节点开始,递归地按照任意顺序访问各个节点的子节点,并不一定要按照从左向右的顺序遍历,之所以称之为深度优先,是因为这种遍历总是尽可能地访问一个节点的尚未被访问的子节点,因此他总是尽可能快的访问离根节点最远的节点(即最深的节点)。

image

3.5 翻译方案

语法制导翻译方案是一种在文法产生式中附加一些程序片段来描述翻译结果的表示方法。

被嵌入到产生式体中的程序片段称为语义动作(semantic action)。一个语义动作用花括号括起来,并写入产生式的体中,他的执行位置也由此制定。

image

前序遍历和后序遍历是深度优先遍历的两种重要特例。在这两种遍历中,我们都是从左到右递归地访问每个节点的子节点。如果动作在我们第一次访问一个节点时被执行,那么我们将这种遍历称为前序遍历(preorder traversal)。类似地,如果动作在最后离开一个节点前被执行,则称这种遍历为后序遍历(postorder traversal)。

image

 

4. 语法分析

大多数语法分析方法都可以归入一下两类:自顶向下(top-down)方法和自底向上(bottom-up)方法。这两个术语指的是语法分析树结点的构造顺序。在自顶向下语法分析器中,构造过程从根结点开始,逐步向叶子结点方向进行;而在自底向上语法分析器中,构造过程从叶子结点开始,逐步构造出根节点。

4.1 自顶向下分析方法

image

 在自顶向下地构造一棵如图所示的语法分析树时,从标号为开始非终结符stmt的根结点开始,反复执行下面两个步骤:

1)在标号为非终结符号A的结点N上,选择A的一个产生式,并为该产生式体中的各个符号构造出N的子结点。

2)寻找下一个结点来构造子树,通常选择的是语法分析树最左边的尚未扩展的非终结符。

对于某些文法,上面的步骤只需要对输入串进行一次从左到右的扫描就可以完成。输入中当前被扫码的终结符号通常称为向前看(lookahead)符号。

如果当前正考虑的语法分析树结点的标号是一个终结符号,而且此终结符号与向前看符号匹配,那么语法分析树的箭头和输入的箭头都向前一步。

image

4.2 预测分析法

递归下降分析方法(recursive-descent parsing)是一种自顶向下的语法分析方法,它使用一组递归过程来处理输入。文法的每个非终结符都有一个相关联的过程。考虑递归下降分析法的一种简单形式,称为预测分析法(predictive parsing)。在预测分析法中,各个非终结符号对应的过程中的控制流可以由向前看符号无二义地确定。

令α是一个文法符号(终结符号或非终结符号)串。我们将FIRST(α)定义为可以由α生成的一个或多个终结符号串的第一符号的集合。如果α就是∈或者可以生成∈,那么∈也在FIRST(α)中。

通常情况下,α要么以一个终结符号开头,此时该终结符号就是FIRST(α)中的唯一符号;要么阿尔法以一个非终结符号开头,且该非终结符的所有产生式体都以某个终结符号开头,那么这些终结符号就是FIRST(α)的所有成员。

4.3 何时使用∈产生式

我们的预测分析器在没有其他产生式可用时,将∈产生式作为默认选择使用。

4.4 设计一个预测分析器

一个预测分析器程序由各个非终结符对应的过程组成。对应于非终结符A的过程完成一下两项任务:

1)检查向前看符号,决定使用A的哪个产生式。

2)然后,这个过程模拟被选中产生式的体。从左边开始逐个“执行”此产生式体中的符号。“执行”一个非终结符号的方法是调用该非终结符号对应的过程,一个与向前看符号匹配的终结符号的“执行”方法则是读入下一个输入符号。

根据翻译方案设计一个语法制导的翻译器:
1)构造一个预测分析器

2)将翻译翻案中的动作拷贝到语法分析器中。如果一个动作出现在产生式p中的文法符号X的后面,则该动作就被拷贝到p的代码中X的实现之后。否则,如果该动作出现在一个产生式的开头,那么它就被拷贝到产生式体的实现代码之前。

4.5 左递归

递归下降语法分析器有可能进入无限循环。当出现如下所示的“左递归”产生式时,分析器就会出现无限循环:

image

 产生式体的最左边的符号和产生式头部的非终结符相同。

考虑两个产生式:

image

 的非终结符号A,其中α和β是不以A开头的终结符号/非终结符号的序列。

因为产生式A -> Aα的右边的最左边符号是A自身,非终结符号A和它的产生式就称为左递归的(left recursive)。

使用一个新的非终结符号R,并按照如下方式改写A的产生式可以达到同样的效果:

image

 非终结符号R和它的产生式R->αR是右递归的,因为这个产生式的右部的最后一个符号就是R本身。

5. 简单表达式的翻译

image

 上图中的翻译方案定义了将要执行的翻译过程。

5.1 抽象语法和具体语法

在一个表达式的抽象语法树(abstract syntax tree)中,每个内部结点代表一个运算符,该结点的子结点代表这个运算符的运算分量。更一般的情况,处理任意的程序设计语言构造时,我们可以创建一个针对这个构造的运算符,并把这个构造的具有语义信息的组成部分作为这个运算符的运算分量。

image

抽象语法树也简称语法树(syntax tree)。

在语法分析树中,内部结点代表的是非终结符号。有时也把语法分析树称为具体语法树(concrete syntax tree),而相应的文法称为该语言的具体语法(concrete syntax)。

5.2 调整翻译方案

左递归消除技术同样可以应用于包含了语义动作的产生式。

嵌入在产生式中的语义动作在转换时被当作终结符号直接进行复制。

image

5.3 非终结符号的过程

image

 图中的函数expr、term和rest实现了语法制导翻译方案。这些函数模拟了对应于非终结符号的各个产生式体。

5.4 翻译器的简化

image

 这个化简将把过程rest展开到expr中。在翻译具有多个优先级的表达式时,这样的化简处理可以减少需要使用的过程数目。

某些递归调用可以被替代为迭代。如果一个过程体中执行的最后一条语句是对该过程的递归调用,那么这个调用就称为是尾递归的(tail recursive)。

5.5 完整的程序

image

6. 词法分析

一个词法分析器从输入中读取字符,并将它们组成词法单元对象。除了用于语法分析的终结符号之外,一个词法单元对象还包含一些附加信息,这些信息以属性值的形式出现。

构成一个词法单元的输入字符序列称为词素(lexem)。

image

6.1 剔除空白和注释

如果词法分析器消除了空白,那么语法分析器就不必再考虑它们了。

6.2 预读

在决定向语法分析器返回哪个词法单元之前,词法分析器可能需要预先读入一些字符。例如,C或Java的词法分析器在遇到字符>之后必须预先读入一个字符。如果下一个字符是=,那么>就是字符序列>=的一部分,否则>本身形成了大于运算符。

6.3 常量

要使得表达式中可以出现整数常量,我们可以创建一个代表整型常量的终结符号,比如num,也可以将整数常量的语法加入到文法中。

当在输入流中出现一个数位序列时,词法分析器将向语法分析器传送一个词法单元。该词法单元包含终结符号num及根据这些数位计算得到的整型属性值。

6.4 识别关键字和标识符

大多数程序设计语言使用for、do、if这样的固定字符串作为标点符号或者用于标识某种构造。这些字符串称为关键字(keyword)。

字符串还可以作为标识符,来为变量、数组、函数等命名。为了简化语法分析器,语义的文法通常把标识符当作终结符号进行处理,如id。

只有当一个字符串不是关键字时它才能组成一个标识符。

6.5 词法分析器

image

 

image

 

image

7. 符号表

符号表是一种供编译器用于保存有关源程序构造的各种信息的数据结构。这些信息在编译器的分析阶段被逐步收集并放入符号表,它们在综合阶段用于生成目标代码。符号表的每个条目中包含与一个标识符相关的信息,比如它的字符串(或者词素)、类型、存储位置和其他相关信息。

7.1 为每个作用域设置一个符号表

术语作用域(scope)本身是指一个或多个声明起作用的程序部分。

在程序的不同部分,可能会处于不同的目的而多次声明相同的标识符。

如果程序块可以嵌套,那么同一个标识符的多次声明就可能出现在同一个块中。

块的符号表的实现可以利用作用域的最近嵌套规则。

语句块的最近嵌套规则是说,一个标识符x在最近的x声明的作用域中。

符号表(Env)的实现,三种操作:

1)创建一个新符号表。

2)在当前表中加入一个新的条目。三列表保存了键-值对,其中

  键是一个字符串。也可以使用指向对应于标识符的词法单元对象的引用作为键。

  值是一个Symbol类的条目。

3)得到一个标识符的条目。它从当前块的符号表开始搜索链接符号表。返回一个符号表条目或者null。

7.2 符号表的使用

从效果上看,一个符号表的作用是将信息从声明的地方传递到实际使用的地方。当分析标识符x的声明时,一个语义动作将有关x的信息“放入”符号表中。然后,一个像factor -> id这样的产生式的相关语义动作从符号表中“取出”这个标识符的信息。

image

 

 

 

 

8. 生成中间代码

编译器的前端构造出源程序的中间表示,而后端根据这个中间表示生成目标程序。

8.1 两种中间表示形式

两种最重要的中间表示形式是:

1)树型结构,包括语法分析树和(抽象)语法树。

2)线性表示形式,特别是“三地址代码”。

除了创建一个中间表示外,编译器前端还会检查源程序是否遵循源语言的语法和语义规则。这种检查被称为静态检查(static check),“静态”一般是指“由编译器完成”。

8.2 语法树的构造

image

image

8.3 静态检查

静态检查是指在编译过程中完成的各种一致性检查。静态检查包括:

1)语法检查。任何作用域内同一个标识符最多只能声明一次,一个break语句必须处于一个循环或switch语句之内。

2)类型检查。一种语言的类型规则确保一个运算符或函数被应用到类型和数量都正确的运算分量上。如果必须要进行类型转换,比如将一个浮点数与一个整数相加时,类型检查器就会在语法树中插入一个运算符来表示这个转换。

左值和右值

i = 5;

表达式的右部描述了一个整数值,而左部描述的是用来存放该值的位置。术语左值(l-value)和右值(r-value)分别表示可以出现在赋值表达式左部和右部的值。

类型检查确保一个构造的类型符合其上下文对它的期望。

类型检查规则按照抽象语法中运算符/运算分量的结构进行描述。

自动类型转换。当一个运算分量的类型被自动转换为运算符所期望的类型时,就发生了自动类型转换(coercion)。

重载。Java中的运算符+应用于整数运算分量时表示相加,而应用于字符串运算分量时表示连接。如果一个符号在不同的上下文中有不同的含义,那么我们说这个符号是重载(overloading)的。

8.4 三地址码

三地址指令

三地址代码是由如下形式的指令组成的序列

x = y op z

其中x、y和z可以是名字、常量或由编译器生成的临时量;而op表示一个运算符。

数组将由下面的两种变体指令来处理:

x[y] = z

x = y[z]

前者将z的值保存到x[y]所指示的位置上,而后者将y[z]的值放到位置x上。

跳转指令:

ifFalse x goto L 如果x为假,下一步执行标号为L的指令

ifTrue x goto L 如果x为真,下一步执行标号为L的指令

goto L  下一步执行标号为L的指令

在一个指令前加上前缀L: 就表示将标号L附加到该指令。同一指令可以同时拥有多个标号。

赋值指令:如下的三地址指令将y的值拷贝至x中

x = y

语句的翻译

image

image

表达式的翻译

image

image

 

posted @ 2025-04-07 22:43  明er  阅读(62)  评论(0)    收藏  举报