编译器设计-符号表-中间代码生成
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
编译器设计-符号表-中间代码⽣成
编译器设计-符号表-中间代码⽣成
Compiler Design - Symbol Table
Compiler - Intermediate Code Generation
⼀.Compiler Design - Symbol Table
符号表是编译器为存储变量名、函数名、对象、类、接⼝等各种实体的出现情况⽽创建和维护的⼀种重要的数据结构。
符号表既可⽤于编译器的分析部分,也可⽤于编译器的综合部分。
符号表可⽤于以下⽬的,具体取决于所使⽤的语⾔:
将所有实体的名称以结构化形式存储在⼀个位置。
以验证是否已声明变量。
要实现类型检查,请验证源代码中的赋值和表达式在语义上是否正确。
确定名称的作⽤域(作⽤域解析)。
符号表只是⼀个可以是线性表或哈希表的表。
它以以下格式为每个名称维护⼀个条⽬:
<symbol name, type, attribute>
例如,如果符号表必须存储有关以下变量声明的信息:
static int interest;
然后它应该存储条⽬,例如:
<interest, int, static>
attribute⼦句包含与名称相关的条⽬。
实施Implementation
如果编译器要处理少量数据,那么符号表可以实现为⽆序列表,这很容易编码,但它只适⽤于⼩表。
符号表可以通过以下⽅式之⼀实现:
线性(排序或未排序)列表
⼆叉搜索树
哈希表
其中,符号表主要实现为哈希表,其中源代码符号本⾝被视为哈希函数的键,返回值是关于符号的信息。
操作Operations
符号表(线性或哈希)应提供以下操作。
插⼊()
此操作在分析阶段使⽤得更频繁,即编译器的前半部分,其中标识了标记并将名称存储在表中。
此操作⽤于在符号表中添加有关源代码中出现的唯⼀名称的信息。
存储名称的格式或结构取决于⼿头的编译器。
源代码中符号的属性是与该符号关联的信息。
此信息包含有关符号的值、状态、范围和类型。
函数的作⽤是:将符号及其属性作为参数,并将信息存储在符号表中。
For example:
int a;
应该由编译器处理为should be processed by the compiler as:
insert(a, int);
查找()
lookup()操作⽤于搜索符号表中的名称以确定:
如果符号存在于表中。
如果在使⽤前声明。
如果在作⽤域中使⽤了该名称。
如果符号已初始化。
如果符号声明了多次。
lookup(symbol)
lookup()函数的格式因编程语⾔⽽异。
基本格式应符合以下要求:
如果符号表中不存在该符号,则此⽅法返回0(零)。
如果符号存在于符号表中,则返回存储在表中的其属性。
范围管理
编译器维护两种类型的符号表:⼀种全局符号表,它可以被所有过程访问,另⼀种是为程序中的每个作⽤域创建的作⽤域符号表。
为了确定名称的范围,符号表按层次结构排列,如下例所⽰:
. . .
int value=10;
void pro_one()
{
int one_1;
int one_2;
{ \
int one_3; |_ inner scope 1
int one_4; |
} /
int one_5;
{ \
int one_6; |_ inner scope 2
int one_7; |
} /
}
void pro_two()
{
int two_1;
int two_2;
{ \
int two_3; |_ inner scope 3
int two_4; |
} /
int two_5;
}
. . .
上述程序可以⽤符号表的层次结构表⽰:
全局符号表包含⼀个全局变量(int值)的名称和两个过程名称,这些名称应该对上⾯显⽰的所有⼦节点都可⽤。
pro_one symbol表(及其所有⼦表)中提到的名称不适⽤于pro_two symbol及其⼦表。
此符号表数据结构层次结构存储在语义分析器中,当需要在符号表中搜索名称时,将使⽤以下算法进⾏搜索:
⾸先在当前范围内搜索⼀个符号,即当前符号表。
如果找到名称,则搜索完成,否则将在⽗符号表中搜索,直到,
找到该名称或已在全局符号表中搜索该名称。
⼆.Compiler - Intermediate Code Generation
⼀个源代码可以直接翻译成它的⽬标机器代码,那么我们为什么要把源代码翻译成⼀个中间代码,然后再翻译成它的⽬标代码呢?让我们看看需要中间代码的原因。
如果编译器在没有⽣成中间代码的选项的情况下将源语⾔转换为⽬标机器语⾔,那么对于每台新机器,都需要⼀个完整的本机编译器。
中间代码通过保持所有编译器的分析部分相同,消除了对每台唯⼀计算机使⽤新的完整编译器的需要。
编译器的第⼆部分,synthesis,是根据⽬标机器⽽改变的。
通过在中间代码上应⽤代码优化技术,可以更容易地应⽤源代码修改来提⾼代码性能。
中间表⽰法Intermediate Representation
中间代码可以⽤多种⽅式表⽰,它们有⾃⼰的优点。
⾼级别的IR-⾼级别的中间代码表⽰⾮常接近源语⾔本⾝。
它们可以很容易地从源代码⽣成,我们可以很容易地应⽤代码修改来提⾼性能。
但对于⽬标机优化,则不太可取。
低级别的IR-这是⼀个接近⽬标机器,这使得它适合于寄存器和内存分配,指令集选择等。
这是⼀个很好的机器相关的优化。
中间代码可以是特定于语⾔的(例如,Java的字节码)或独⽴于语⾔的(三地址码)。
三地址码 Three-Address Code
中间代码⽣成器以带注释的语法树的形式接收来⾃其前⼀阶段语义分析器的输⼊。
然后,可以将该语法树转换为线性表⽰法,例如后缀表⽰法。
中间代码往往是与机器⽆关的代码。
因此,代码⽣成器假设有⽆限数量的内存存储(寄存器)来⽣成代码For example:
a =
b +
c * d;
中间代码⽣成器将尝试将此表达式划分为⼦表达式,然后⽣成相应的代码。
r1 = c * d;
r2 = b + r1;
a = r2
r⽤作⽬标程序中的寄存器。
⼀个三地址码最多有三个地址位置来计算表达式。
三地址码可以⽤两种形式表⽰:四位和三位。
四倍Quadruples
四位数表⽰中的每条指令分为四个字段:operator、arg1、arg2和result。
以上⽰例以四位数格式表⽰如下:
三元组
三元组表⽰中的每条指令都有三个字段:op、arg1和arg2。
各⼦表达式的结果由表达式的位置表⽰。
三元组表⽰与DAG和语法树的相似性。
它们在表⽰表达式时等价于DAG。
三元组在优化时⾯临代码不可移动的问题,因为结果是位置的,更改表达式的顺序或位置可能会导致问题。
间接三元组
此表⽰是对三元组表⽰的增强。
它使⽤指针⽽不是位置来存储结果。
这使优化器能够⾃由地重新定位⼦表达式以⽣成优化的代码。
声明 Declarations
变量或过程在使⽤之前必须声明。
声明涉及内存空间的分配以及符号表中类型和名称的输⼊。
⼀个程序的编码和设计可以考虑到⽬标机器的结构,但可能并不总是能够准确地将源代码转换成⽬标语⾔。
将整个程序作为过程和⼦过程的集合,就可以声明过程的所有本地名称。
内存分配是以连续的⽅式完成的,名称是按照在程序中声明的顺序分配给内存的。
我们使⽤offset变量并将其设置为零{offset=0},表⽰基址。
源程序设计语⾔和⽬标机器体系结构的名称存储⽅式可能不同,因此使⽤相对寻址。
当从内存位置0{offset=0}开始分配第⼀个名称时,后⾯声明的下⼀个名称应该在第⼀个名称旁边分配内存。
例⼦:
我们以C语⾔为例,其中⼀个整数变量被分配2字节的内存,⼀个浮点变量被分配4字节的内存。
int a;
float b;
Allocation process:
{offset = 0}
int a;
id.type = int
id.width = 2
offset = offset + id.width
{offset = 2}
float b;
id.type = float
id.width = 4
offset = offset + id.width
{offset = 6}
要在符号表中输⼊此详细信息,可以使⽤过程enter。
该⽅法可以具有以下结构:
enter(name, type, offset)
此过程应在符号表中为变量名创建⼀个条⽬,将其类型设置为类型,并在其数据区域中设置相对地址偏移量。