7.2 声明语句
在编译器设计中,处理声明语句是构建符号表和分配存储空间的关键步骤。声明语句不仅定义了变量的名称和类型,还影响了变量在内存中的布局。本节讨论的是如何在编译过程中处理过程内的声明,以及如何为局部变量分配相对地址。
处理过程中的声明
当编译器遇到过程(函数或方法)中的声明语句时,它会执行以下步骤:
-
为局部变量建立符号表条目:编译器会为过程中声明的每个局部变量创建一个符号表条目。这些条目包含变量的类型、名称,以及相对于某个基址的偏移量等信息。
-
分配存储单元:编译器根据变量的类型确定需要分配的存储空间大小,并计算相对地址。这个地址是相对于静态数据区的基址或活动记录中某个基址的偏移量。
目标机器的特点
在分配地址时,编译器必须考虑到目标机器的特点,如数据对象的对齐要求。虽然本讨论中忽略了对齐等问题,但在实际应用中,这些是重要的考虑因素。
示例:声明序列的处理
以下是图7.5中的翻译方案,展示了如何处理声明序列:
- 初始化偏移量:在处理第一个声明之前,将偏移量
offset
设置为0。 - 分配存储单元:为每个声明的变量分配存储空间,并更新
offset
。 - 符号表条目:使用
enter
函数为每个变量在符号表中创建条目,记录变量的类型和相对地址。
类型和宽度
- 基本类型:整数(
integer
)宽度设为4字节,实数(real
)宽度设为8字节。 - 数组类型:数组的宽度是其元素宽度与元素个数的乘积。
- 指针类型:指针的宽度假定为4字节。
通过这种方法,编译器为每个局部变量分配了确切的存储位置,并记录了必要的类型信息,为后续的代码生成和优化提供了基础。正确处理声明语句对于生成有效和正确的目标代码至关重要。
7.2.2 作用域信息的保存
在支持过程嵌套的语言中,如Pascal,正确处理作用域和名称解析是编译器设计中的关键问题。为了管理嵌套过程中的声明和作用域,编译器需要在每个过程中为局部变量和过程本身建立符号表,并正确处理这些符号表之间的关系。
符号表和作用域
符号表的结构
每个过程都有自己的符号表,其中包含该过程内声明的所有局部变量和嵌套过程的信息。这些符号表不仅记录变量的类型和相对地址,还包括指向外围(父)过程符号表的指针,形成一个符号表的链。
处理嵌套过程
当编译器遇到一个嵌套过程声明时,它会暂停当前过程的处理,为嵌套过程创建一个新的符号表,并将控制权转移到该嵌套过程。为了管理这种嵌套结构,编译器使用两个栈:
- 符号表栈:存储当前未完成处理的过程的符号表指针。
- 偏移量栈:记录每个过程中下一个可用的相对地址(偏移量)。
通过这种方式,编译器可以正确处理过程嵌套,并在返回到外围过程时恢复其上下文。
语义动作
在编译器的语义动作中,以下函数和操作用于管理作用域和符号表:
mhTable(previous)
:创建一个新的符号表,并将其指向先前的符号表,以反映过程的嵌套结构。enter(table, name, type, offset)
:在给定的符号表中为变量或过程名创建一个新条目。addWidth(table, width)
:记录给定符号表中所有局部变量所需的存储单元总数。enterProc(table, name, newTable)
:为过程名在符号表中创建一个新条目,链接到该过程自己的符号表。
符号表的应用
通过建立每个过程自己的符号表,并通过符号表栈管理嵌套的作用域,编译器能够正确解析名称,处理变量和过程的作用域,并为变量和过程分配适当的存储空间。当编译器处理完一个过程后,它可以通过符号表链回溯到外围过程,继续之前的处理流程。
此外,符号表的这种组织方式也便于实现对预定义标识符的支持,如Pascal语言中的integer
、true
等,通过将最外层作用域的符号表逆向指向标准标识符的符号表,编译器可以轻松地解析这些标准标识符。
总之,妥善管理作用域信息和符号表是支持过程嵌套和正确解析变量作用域的关键,这对于生成正确的目标代码和支持语言特性(如过程嵌套、局部变量声明等)至关重要。
7.2.3 记录的域名
在编译器处理记录(或结构体)类型定义时,需要为记录中的每个域(字段)建立符号表条目,并计算记录类型的总宽度以及每个域在记录内的相对位置。这与处理过程中局部变量的声明相似,但专注于记录类型的内部结构。
扩展语法以支持记录类型
语法被扩展以支持记录类型的定义,通过添加产生式 T→record D end
,使得非终结符 T
不仅可以产生基本类型、指针和数组类型,还能产生记录类型。此扩展允许在记录类型定义中声明域名。
符号表的建立与布局
符号表的建立
- 在遇到
record
关键字时,编译器为记录类型中的每个域建立一个新的符号表。 - 新建的符号表指针被压入符号表栈(
tblStack
),而相对地址0
被压入偏移量栈(offsetStack
)。
存储布局
- 对于每个域的声明
D→id:T
,编译器把该域的信息(包括类型和相对地址)加入记录类型的符号表。 - 记录类型中所有对象的总宽度由
offsetStack
栈顶的值给出。
宽度和类型属性的返回
- 记录定义结束时,
offsetStack
栈顶的值即为记录类型的总宽度,这个值作为综合属性T.width
返回。 - 同时,通过将类型构造器
record
应用于记录的符号表指针,可以得到T.type
,这表示记录类型的内部结构。
记录类型与过程声明的区别
处理记录类型定义时,编译器不为变量分配存储单元,而是确定记录类型的总宽度和每个域的相对位置。这与过程声明中的局部变量处理不同,后者涉及到实际的存储单元分配。记录类型变量的存储单元分配发生在该记录类型变量被声明时,此时编译器根据记录类型的宽度和内部域的布局在活动记录中为变量分配空间。
结论
通过在编译阶段处理记录类型定义,编译器能够有效管理记录内部的域名和布局。这为之后的变量声明和内存分配提供了必要的信息,确保了记录类型变量在内存中的正确布局。此过程不仅增强了编译器对高级数据结构的支持,还为生成高效的目标代码打下了基础。