7.3 赋值语句
在编译器设计中,赋值语句的处理是基础且关键的一环,它涉及到变量的查找、类型检查、数组元素和记录域的访问等多个方面。本节将深入探讨如何将赋值语句翻译成三地址代码,并处理相关的符号表查找和访问。
7.3.1 符号表中的名字
在编译过程中,变量名在三地址代码中通常通过指向符号表中条目的指针来表示。这一步骤是必要的,因为它允许编译器获取变量的类型、存储位置和其他属性信息。
查找过程
当编译器遇到变量名时,它会调用 lookup
函数,在符号表中查找该名字的条目:
- 如果找到,
lookup
函数返回该条目的指针。 - 如果未找到,返回
nil
,表示该变量未声明。
示例翻译方案
图7.9展示了一个翻译方案,其中包含了如何为赋值语句生成三地址代码:
- 赋值语句 (
S→id:=E
):首先查找左侧变量的符号表条目,如果存在,则生成赋值的三地址代码。 - 表达式 (
E→E1+E2
):对于加法表达式,创建一个新的临时变量来存储结果,并生成相应的三地址代码。
生成临时变量
newTemp
函数用于生成新的临时变量,并将其添加到符号表中。生成的临时变量用于存储表达式计算的中间结果。
输出三地址指令
emit
过程用于将三地址指令写入到输出文件中,它接受构成一条指令的参数。
记录域的访问
当需要访问记录的域时,如 p.info
:
- 首先在符号表中找到记录变量
p
的条目。 - 通过
p
的类型信息,访问记录类型的符号表,查找域info
的条目。 - 从
info
的条目中获取所需的属性,如类型和相对地址。
这种方法允许编译器正确处理记录类型变量的域访问,确保访问的正确性和有效性。
总结
处理赋值语句和变量访问是编译器设计中的核心任务之一。通过在符号表中维护变量和类型信息,以及生成三地址代码,编译器能够有效地转换高级语言构造为机器或虚拟机可执行的指令序列。此外,符号表的结构和查找机制是支持复杂数据结构(如数组和记录)及其操作的基础,对于生成优化和正确的代码至关重要。
7.3.2 数组元素的地址计算
在编译器中处理数组元素的地址计算是关键的,尤其是对于多维数组,因为它们的存储通常是连续的,而元素的访问需要计算出具体的内存地址。一维和多维数组的元素地址计算有其特定的公式,这些公式考虑了数组的维度、元素的宽度、下界和上界等因素。
一维数组的地址计算
对于一维数组 A
,元素 A[i]
的地址计算公式为:
base + (i - low) * w
其中:
base
是数组A
分配的起始地址。low
是数组的下界。w
是数组每个元素的宽度。
二维数组的地址计算
对于二维数组 A[i, j]
存储在行主序时,地址计算公式为:
base + ((i - low1) * n2 + (j - low2)) * w
其中:
low1
和low2
分别是两个维度的下界。n2
是第二维的元素个数。w
是数组每个元素的宽度。
多维数组的地址计算
多维数组地址的计算可以推广至更高维度。对于行主序存储的 d
维数组 A[i1, i2, ..., id]
,地址计算公式为:
((…((i1 * n2 + i2) * n3 + i3)…) * nd + id) * w
其中 nj
表示第 j
维的大小,w
是数组每个元素的宽度。
编译时与运行时的计算
对于静态数组,即在编译时大小已知的数组,很多地址计算的成分可以在编译时完成。这包括固定的上下界、维度大小等,从而减少运行时的计算负担。然而,对于动态数组,即其大小在运行时才确定的数组,地址计算必须全部在运行时进行。
优化地址计算
编译器可以通过预计算表达式中的静态部分来优化地址计算。例如,对于表达式 (i * n2 + j) * w
,如果 n2
和 w
在编译时已知,那么 (low1 * n2 + low2) * w
这部分可以预先计算,从而简化运行时的计算。
结论
数组元素的地址计算是编译器设计中的一个重要方面,尤其是对于多维数组。通过精确的地址计算公式,编译器能够生成有效的代码来快速访问数组元素,同时通过编译时的优化减少运行时的计算负担。对于动态数组,虽然地址计算必须在运行时进行,但通过合理的设计,仍然可以保证访问的效率。
7.3.3 数组元素地址计算的翻译方案
本节介绍如何将多维数组引用的地址计算转换为三地址代码,这是编译器在处理数组类型数据时必须完成的任务。对于多维数组 A[i1, i2, …, ik]
,其元素的地址计算涉及到多步乘加操作,最终将这些操作转换成一系列的三地址指令。
基本思路
- 初始化:对于数组引用的第一个维度
i1
,直接将其值赋给一个临时变量e1
。 - 递推计算:对于每个后续的维度
im
,根据递推公式em = em-1 * nm + im
生成对应的乘法和加法三地址指令,直到处理完所有维度。
处理数组引用
对于语句 S→L:=E
:
- 如果
L
是简单变量,则生成常规的赋值指令。 - 如果
L
是数组引用,则生成索引赋值指令,涉及到通过L.offset
和L.place
确定的存储单元。
对于表达式 E→L
(当 L
是数组元素时):
- 如果
L.offset
不为null
,则E
的值需要通过索引L.place[L.offset]
获取,这需要生成相应的索引读取指令。
数组地址计算示例
考虑一个二维数组 A
,其引用 A[y, z]
被转换成以下三地址代码:
t1 = y * n2
(计算第一个下标y
对应的偏移量,n2
是第二维的大小)t2 = t1 + z
(将t1
与第二个下标z
的值相加,得到总偏移量)t3 = base - (low1 * n2 + low2) * w
(计算编译时可确定的偏移量部分)t4 = t3 * w
(将总偏移量乘以元素宽度w
)t5 = t4[t2]
(通过偏移量t2
读取数组元素的值)x = t5
(将数组元素的值赋给x
)
其中,base
是数组的基地址,low1
和 low2
是两个维度的下界,w
是数组元素的宽度。
结论
通过这种方法,编译器能够有效地处理数组引用,并将其转换为相应的三地址代码。这不仅确保了数组元素的正确访问,也为进一步的代码优化提供了基础。正确处理数组元素的地址计算是编译器设计中的一个重要方面,它直接影响到生成代码的效率和正确性。
7.3.4 类型转换
类型转换是编译器中间代码生成阶段处理的重要方面,特别是在处理涉及不同数据类型操作数的表达式时。编译器需要生成额外的代码来进行必要的类型转换,以确保运算的类型安全和正确性。本节讨论如何在生成中间代码时处理整数到实数的自动类型转换。
自动类型转换的处理
考虑到整数(integer
)和实数(real
)两种类型,当在表达式中混合使用这两种类型时,编译器将自动将整数转换为实数进行计算,以保持运算的一致性。这种自动类型转换主要发生在算术运算中,如加法(+
)。
示例:加法表达式的类型转换
对于加法表达式 E→E1+E2
,其语义动作需要根据操作数的类型来决定是否需要进行类型转换,并标记相应的算术运算为定点(整数)或浮点(实数)运算。
语义动作的设计
- 首先,为存储运算结果创建一个新的临时变量
E.place
。 - 如果两个操作数类型均为
integer
,则执行整数加法,并将结果类型设为integer
。 - 如果两个操作数类型均为
real
,则执行浮点加法,并将结果类型设为real
。 - 如果操作数类型不匹配(一个为
integer
,另一个为real
),则将整数操作数转换为实数,并执行浮点加法。转换操作由三地址指令inttoreal
实现。
类型转换的三地址代码示例
假设 x
和 y
为 real
类型,i
和 j
为 integer
类型,对于表达式 x = y + i * j
,生成的三地址代码序列可能如下:
t1 = i int* j // 整数乘法
t2 = inttoreal t1 // 整数转实数
t3 = y real+ t2 // 浮点加法
x = t3 // 结果赋值给 x
语义动作的实现
图7.11中的语义动作通过使用属性 E.place
和 E.type
来记录表达式的结果位置和类型。根据操作数的类型,适当地生成 inttoreal
转换指令和标记为 int+
或 real+
的加法指令。
结论
自动类型转换是编译器中间代码生成阶段的一个重要功能,它确保了类型不匹配的表达式能够被正确处理。通过在需要时插入类型转换指令,编译器能够生成正确的代码来执行预期的运算,同时保持类型的一致性和安全性。这一过程涉及到细致的语义分析和适当的代码生成策略,以确保最终生成的代码既高效又准确。