简言之,数组元素操作数的翻译就是按照地址计算公式将下标引用形式转换为逻辑地址形式。在正式分析源码之前,先来看一个实例。
例6-3 根据输入源程序生成相应的IR序列。
输入源程序如下:
在表6-7中,列出了两个完整的IR序列。其中,左列是不经过任何优化的IR序列,它就是由语义子程序直接生成的,并没有作任何加工。而右列是经过优化后的IR序列,从优化效果来看,主要是简化了一些冗余的类型转换IR。虽然右列的IR比较精简,但是它丝毫不失为分析数组元素翻译的好例子。下面将基于右列的IR作相关讨论。计算公式如下:
表6-7 数组寻址的实例
第0行:计算clxlen2,并将结果送入TO。Cl即为第1维的下标i,而len2即为第2维的长度。
第1行:计算C2+_TO,并将结果送入T2。其中,c2即为第2维的下标j。
第2行:计算公式中第3项与公式中第2项的差,将结果送入T3。其中,第2项常量的值即为6。
第3行:将T3xw,并将结果送入T5。其中,w即为integer类型数据占用的空间数。
第4行:获取数组A的首地址,即为BaseA,将结果送入T7。
第5行:将T5与T7相加,即获得了A[i,j]的地址,将计算结果送回T7。
第6行:通过对T1的间接寻址,将常量送入A[i,i]中。
至此,通过一个实例,读者应该已经了解了数组元素的寻址问题。下面,就来看看数组元素操作数的相关文法:
【文法6-5】
其中,数组下标表达式的有效性检查主要包括以下几项:
(1)根据Pascal语言的规定,数组的下标必须为整型表达式。
(2)越界访问检测。Neo Pascal只进行静态检测,不进行动态检测。
(3)下标的个数必须与数组的维数一致。例如,试图用“A[l]”或“A[l,2,3]”的方式引用二维数组A都是非法的。
此外,由于“表达式列表”是一个复用非终结符,所以需要semantic098、semantic099根据实际情况设置语义处理标志。这种思想在处理“标识符列表”相关语义时,已经详细解释过了,读者应该并不陌生。
程序6-13 semantic.cpp
从完整的文法来看,不难发现,semantic055的另一个功用就是处理集合变量的表达式列表。因此,借助于iExpListFlag栈传递处理标志,告知semantic055所需完成的语义动作。当iExpListFlag栈顶元素为1时,表示“表达式列表”是由“变量1”的候选式推导出来的,此时,semantic055的语义动作就是处理数组的下标。否则,表示“表达式列表”是由“因子”的候选式推导出来的,那么,semantic055的语义动作就是处理集合变量的值。
第5~9行:语义有效性检查。其中,Operand栈中保存的是下标表达式的结果操作数,而CurrentVar栈中保存的是数组对象的信息。另外,对常量符号取下标也是无意义的。
第14行:获取Operand栈顶元素,该元素即为当前下标表达式的结果操作数。
第16行:获取CurrentVar栈顶元素,该元素即为当前操作数。由于semantic055处理的表达式必须是数组第1维的下标,因此,当前操作数的m_VarTypeStack栈顶类型一定是数组。与C语言不同,Pascal严格限制下标运算只能应用于数组。
第17行:设置m_iDim属性。这个属性主要用于记录当前已经分析完成的下标表达式个数。根据地址计算公式,不难发现,其中leni的取值完全依赖于下标表达式的序号。因此,将下标表达式的个数暂存在m_iDim属性中是有必要的。
第18行:获取当前变量的类型信息。
第19行:判断下标表达式是否为常量。由于semantic055仅用于处理第1维的下标表达式,所以只需要考虑当前下标表达式是否为常量即可。
第21--25行:下标表达式的有效性检查,包括两项判断:第一项,下标的类型是否为整型;第二项,下标是否越界访问。由于下标表达式是常量,所以可以进行静态越界访问检测。
第28~35行:计算偏移量。注意,这里需要考虑当前维度是否为最末的维度。根据公式可知,除最末维度之外,都需要乘以下一维度的len。由于semantic055只处理第一维的偏移,因此,只需判断数组维度是否大于或等于2即可。
第36、37行:生成常量操作数。
第38~43行:这个条件语句是非常重要的。先前所讨论的情况都只是以数组首地址为基准考虑偏移的,并没有涉及数组首地址本身的偏移。换句话说,数组的首地址也可能是相对于某个变量符号的偏移。例如,数组作为记录的一个字段,那么,数组的首地址就是相对于该记录变量的一个常量偏移。因此,如果当前符号本身存在偏移时,就需要将其保存在一个暂存空间中。在计算数组首地址时,再将其取出。
第44行:将常量操作数压入Operand栈。
第54~79行:处理下标表达式不是常量的情况。(www.daowen.com)
第54~58行:下标表达式的有效性检查。由于表达式不是常量,因此不需要考虑静态越界检查。
第61~70行:生成IR用于计算C。xlen。+1的值。
第72~77行:与第38--43行的作用相同,暂存数组本身的偏移。
第78行:将常量操作数压入Operand栈。
程序6-14 semantic.cpp
semantic057的功能与semantic055是基本类似的,不过,需要考虑叠加前一维计算得到的偏移量。同样,semantic055也存在复用的情况,因此,也需要借助于iExpListFlag栈传递处理标志,其基本思想与semantic055是完全一致的。
第5~9行:语义有效性检查。注意,根据文法,在这种情况下,Operand栈中必须至少存在两个操作数,即前一维计算得到的偏移量及当前的下标表达式。否则,是不符合语义的。而CurrentVar栈中保存的是数组对象的信息。当然,对常量符号取下标也是无意义的。
第13行:获取下标表达式的结果操作数。根据栈的性质,这是必然的。
第15行:获取前一维计算得到的偏移量操作数。
第17行:获取CurrentVar栈顶元素,该元素即为当前操作数。
第18行:设置m_iDim属性。这个属性主要用于记录当前已经分析完成的下标表达式个数。根据地址计算公式,不难发现,其中len,的取值完全是依赖于下标表达式的序号。因此,将下标表达式的个数暂存在m_iDim属性中是有必要的。
第19~23行:判断是否存在维度越界的情况,即访问的维度大于声明的维度。在处理semantic055时,并不需要考虑,这是因为数组至少是一维的。
第24行:判断是否满足常量折叠的条件,即前一维计算得到的偏移与当前下标表达式的结果都是常量。注意,仅有这种情况才能进行常量折叠,否则,只能通过生成IR计算偏移。
第26~30行:下标表达式的有效性检查,包括两项判断:第一项,下标的类型是否为整型;第二项,下标是否越界访问。由于下标表达式是常量,所以可以进行静态越界访问检测。
第32~47行:处理与semantic055类似,根据公式求得当前维度的偏移值。这里同样需要考虑当前维度是否为最末的维度。根据公式可知,除最末维度之外,都需要乘以下一维度的len。
第57~61行:下标表达式的有效性检查。由于表达式不是常量,因此不需要考虑静态越界检查。
第62~65行:生成前一维的偏移与当前下标的求和IR,并申请一个临时变量用于保存这个计算结果。
第66行:将计算结果操作数压入Operand栈。
第67~82行:基本思想与第35~39行的处理类似,只不过这里生成的是IR指令,而不是常量偏移值。
至此,笔者详细分析了semantic055、semantic057的主要功能及实现细节。最后,再来看看semantic059的基本实现。与先前两个语义子程序相比,semantic059稍复杂,它是数组元素寻址翻译的最后一个部分。
程序6-15 semantic.cpp
第3~7行:语义有效性检查。当然,对常量符号取下标也是无意义的。
第8行:获取CurrentVar栈顶元素,该元素即为当前操作数。
第10~14行:判断访问维度与数组声明维度是否一致,Pascal语言对此是有严格要求的。
第17~25行:根据公式计算其中的不变部分,将其临时保存在iArrayLowC变量中。
第26行:由类型系统计算得到每个数组元素所占用的空间大小。
第31~39行:处理偏移为常量的情况。
第32~34行:更新当前操作数的类型,也就是将数组元素的类型压入m_VarTypeStack栈。注意,这里的类型跟踪是非常重要的,有助于编译器及时了解当前操作数的实际类型。
第35~36行:更新当前操作数的偏移信息。这里,生成一个常量符号作为偏移量即可。
第42~66行:处理偏移为变量的情况。
第42~49行:根据公式生成IR,计算偏移操作数与iArrayLowC的差值。
第50~54行:获取数组元素所占用空间的大小。根据公式生成IR,计算最终的实际偏移。
关于数组元素操作数的语义处理,暂且讨论至此。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。