在前文对语义检查进行简介时,我们已初步介绍过用于对二元运算符表达式进行语义检查的函数CheckBinaryExpression,为了阅读方便,这里我们再次给出图4.2.2。在本小节中,我们准备对第1126至1144行中的各个函数进行讨论。
图4.2.2 CheckBinaryExpression()
对于形如a+b的二元运算表达式,我们要通过在前面章节中介绍的函数CommonRealType来求得整个表达式a+b的类型,如果a为int型且b为double型,则表达式a+b的类型为double。在图4.2.40中给出的宏定义PERFORM_ARITH_CONVERSION中,第14行调用了CommonRealType函数用来求公共类型,而第15和第16行则分别对二元运算符的左操作数和右操作数进行必要的转型操作。第6行的宏SWAP_KIDS用于交换左右操作数,例如当遇到形如i
+ ptr的表达式,其中i为整数,而ptr为指针,我们如果希望左操作数为指针类型,而右操作数为整数类型,则可使用宏SWAP_KIDS来交换左右操作数,进而得到ptr+i。第19行的REPORT_OP_ERROR用于报错,表示遇到了非法的运算符。
图4.2.40 宏PERFORM_ARITH_CONVERSION
接下来我们来分析一下图4.2.2第1126至1143行中列出的CheckEqualityOP等函数,其代码如图4.2.41所示。第2行我们给出了用于判断是否为算术类型的宏IsArithType,其定义在ucl\type.h,第3行的IsScalarType用于判断类型是否为标量类型。结合第3行的POINTER等枚举常量及其在type.h中的枚举定义,不难读懂type.h中的其他几个宏,如BothScalarType等,这里不再啰嗦。图4.2.41第5至24行的代码用于对形如a==b或a!=b的表达式进行语义检查,如果操作数a和b都是算术类型(即整型和浮点类型),则在第12用宏PERFORM_ARITH_CONVERSION来完成必要的类型转换。表达式a==b或a!=b的结果为真或假,在C语言中,我们用int类型来表示布尔值,所以第13行置表达式的类型为int。第14行调用FoldConstant函数进行必要的常量折叠,例如对于3
== 2这样的表达式,没有必要到运行时再求值,编译时我们就可以知道表达式3 == 2的结果为0。
图4.2.41 CheckEqualityOP()
图4.2.41第16至21行则是按照C标准ucc\ansi.c.txt第3.3.9节” Equality operators”中规定的语义规则进行编写的,如下所示:
(1) both operands are pointers to qualified or unqualified versions ofcompatible types;
两个指针是类型相容的指针,我们在前面的章节中介绍过“相容类型”的概念。对应上图第16行。
(2)one operand is apointer to an object or incomplete type and the other is a qualified orunqualified version of void ; 一个指针是指向数据对象的指针,另一个是void *。对应上图第17和18行。
(3)one operand is apointer and the other is a null pointer constant. 一个是指针类型,而另一个为NULL。对应上图第19和20行。
图4.2.41第25至31行用于处理a&b、a|b和a^b这样的位运算,这些运算符要求操作数为整型;而第32至39行则用于处理形如a && b和a||b这样的短路运算,第34行的if条件要求两个操作数都为标量类型(即整型、浮点型和指针类型等);第41至54行的代码用于对a/b、a*b和a%b进行语义检查,取余运算%要求两个操作数都为整型,而乘除运算则可以是整型或浮点型;第55至61行的函数则用于检查形如a<<b和a>>b这样的移位运算,这里需要注意的是,我们并没有使用宏PERFORM_ARITH_CONVERSION来求公共类型,其原因在于表达式a>>b是进行算术右移还是逻辑右移,要取决于“左操作数a为有符号整数还是无符号整数”,所以在第60行我们置表达式a>>b的类型为第一个操作数a的类型。图4.2.41中的代码不算复杂,我们就不再啰嗦。
接下来,我们来分析一下CheckAddOP等函数,如图4.2.42所示。第1至26行的代码用于处理形如a+b的表达式,而第27至48行的代码则对形如a-b的表达式进行语义检查。对于a+b而言,第9至11行用于处理两个操作数都为算术类型的情况;当一个操作数为指针类型,而另一个操作数为整型,表达式ptr+i进行的是C语言的指针加法运算,在汇编代码层次真正执行的加法为ptr
+ k,其中k为i*sizeof(*ptr),第21行调用的ScalePointerOffset函数用于构造进行乘法运算的语法树结点。而对于a-b来说,第30至33行用于处理减法的两个操作数都是算术类型的情况,第34至39行用于处理形如ptr-i的指针运算,跟ptr+i的指针运算类似,我们需要在第37行调用ScalePointerOffset函数来构造i*sizeof(*ptr)的乘法运算。第41至48行则用于处理形如ptr1-ptr2的指针减法运算,例如,对于intarr[3]而言,&arr[2]-&arr[0]的含义是&arr[2]与&arr[0]之间相差几个int整数,而不是它们的地址相差几个字节,因此在汇编代码层次,我们真正执行的运算为(ptr2-ptr1)/sizeof(*arr),第44行调用的函数PointerDifference用于构造相应的除法运算结点。第49至60行给出了该函数的代码,第53的CREATE_AST_NODE用于创建语法树结点,第55行置其运算符为OP_DIV,即除法。
图4.2.42 CheckAddOP()
下面,我们来讨论赋值运算表达式的语义检查。按照C的语义,在表达式a+=b中,a只被计算一次,而在a = a+b中,a却要被计算两次。例如,对以下代码而言,在表达式*f()+= 3中,函数f()只被调用一次,而在表达式*f() += *f() + 3中,函数f()需要被调用两次。因此,当我们把a += b转换为a = a + b来处理,也要保持这样的语义不变。
int * f(void){
static int number;
printf("int *f(void) \n");
return &number;
}
int main(){
*f() += 3;
*f() += *f() + 3;
return 0;
}
对于在C源代码中就写为a = a + b的表达式来说(为表述方便,不妨记为a1 = a2 +b),在语法树上,有两个结点与a对应,即a1和a2各对应一个语法树结点;但对a+=b而言,只有一个语法树结点与a对应,语义检查时,我们把a+=b转换为a = a’ + b来处理后,并不会为a构造新的语法树结点。为了与在C源代码中就写为a=a+b的表达式有所区别,我们不妨把语义检查后由a+=b得来的表达式写为
a= a’+b,其中a和a’对应的是同一个语法树结点。
有了这个基础后,让我们来分析一下对赋值运算表达式进行语义检查的函数CheckAssignmentExpression。赋值运算符左侧的操作数必须是左值,且该操作数在声明时不应有限定符const,如果左侧操作数是结构体对象,则在该结构体的定义中,所有的成员域都不应有const限定符,图4.2.43第38至44行的CanModify()函数完成了这些判断。第13行调用CanModify()函数来检查一下左操作数是否为可写的左值。第18至27行用于把形如
a += b的表达式转换为a = a’+b来处理,我们只在第20行创建了一个新的语法树结点用来存放运算符+,而a和a’始终对应同一个语法树结点。对于a = a’+b来说,第26行实际上是调用CheckAddOP函数来对加法运算进行语义检查。第29行调用的CanAssign函数用于检测赋值运算符两侧的操作数在类型上是否匹配,我们在前面的章节中分析过CanAssign函数。
图4.2.43 CheckAssignmentExpression()
图4.2.44给出了表达式a+=b在语法分析后和语义检查后的语法树,由该图我们可以很清楚地看到,在把a+=b转换为a=a’+b后,语法树上a对应的结点仍旧只有一个。语义检查后,我们还会在语法树上添加各结点的类型信息,此处为简单起见,我们忽略了这些信息。
图4.2.44 a+=b的语法树
然后,我们来讨论一下对形如a?b:c的条件表达式的语义检查,与其相关的代码如图4.2.45所示。条件表达式实际上可被当作是三元运算符,即有a、b和c这3个操作数。图4.2.45第6至12行用于对第一个操作数a进行语义检查,第9行要求第一个操作数a为标量类型,第13至18行递归地调用CheckExpression函数,来对第2个操作数b和第3个操作数c分别进行语义检查。
图4.2.45 CheckConditionalExpression()
图4.2.45第19至50行的代码,用于对形如a?b:c中的操作数b和c的类型进行检查,这些代码是按照C标准ucc\ansi.c.txt的第“3.3.15 Conditional operator”节的语义规则编写的,如下所示:
(1)both operands have arithmetic type,即b和c的类型都为算术类型,对应上图第
22至28行,此时整个条件表达式的类型为b和c的公共类型。
(2)both operands have compatible structure or union types,即b和c的类型为相容
的结构体或联合体类型,对应上图第29至30行。
(3)both operands have void type,即b和c的类型都为void,对应上图第31至32
行,此时整个条件表达式的类型就为void。
(4)both operands are pointers to qualified or unqualified versions ofcompatible types,
即b和c为相容的指针类型,对应上图第33至36行,此时整个条件表达式的类型为b和c类型的合成类型。
(5)one operand is a pointer and the other is a null pointer constant,即一个为指针类
型,另一个为NULL,对应上图第37至42行。
(6)one operand is a pointer to an object or incomplete type and the otheris a pointer
to aqualified or unqualified version of void,即一个是指向数据对象的指针(即不是指向函数的指针),另一个是void *。
除了以上这6种情况外,b和c的其他类型都被视为非法的,图4.2.45第48行会进行报错。图4.2.45中调用的CommonRealType和CompositeType等函数,我们都已在前面的章节中做过讨论。至此,我们完成了对C语言表达式的语义检查,相关的代码主要在exprchk.c中,可以发现后缀表达式和一元表达式的语义是相对比较复杂的,例如,后缀运算符[]和一元运算符*都涉及到了内存寻址。对这些运算符的语义检查,有助于我们更深入地理解C语言。