4.2.3
在这一小节,我们先来分析一下基本表达式PrimaryExpression的语义检查,由C的标准文法,我们可以知道与PrimaryExpression相关的产生式如下所示,即加了一对小括号的表达式(Expression)在语法上也相当于标志符ID、常量CONST和字符串StringLiteral。
primary-expression:
ID
constant
string-literal
( expression )
例如,对于表达式(a+b)+c而言,(a+b)和c都是基本表达式PrimaryExpression,其语法
地位是相当的,但经语法分析后,我们为(a+b)+c生成的抽象语法树如下所示:
(+ (+ a b) c)
对(+ a b)这棵进行加法运算的语法子树而言,因为其运算符+是二元运算符,所以其语义检查是在CheckBinaryExpression函数中完成的,而语法子树c则是在CheckPrimaryExpression函数中完成。换言之,在对基本表达式PrimaryExpression进行语义检查时,我们只需要考虑标志符ID、字符串StringLiteral和常量constant,而不需要考虑(Expression)。我们先来看一下UCC编译器是如何处理字符串的,如图4.2.8所示。
图4.2.8 字符串StringLiteral
图4.2.8第1至11行的hello.c中共有5个字符串,由第15至20行可见,除了用于初始化全局数组buf[]的”123456”外,UCC编译器为其他的几个字符串取名为.str0、.str1、.str2和.str3。而对字符串”123456”而言,我们就可用全局字符数组buf的名称来为之命名,如图第20行所示。但对第6行的局部数组buf2[]而言,其对应的存储空间在栈中,是在运行时动态分配,因此UCC编译器为第6行的字符串”abcdef”取名为.str3。而buf2[]数组的初始化则需要在运行时由第29至32行的多条汇编指令来完成,与之对比的全局数组buf[]的初始化在编译时就已经完成,如图第19至20行所示。第3行的字符串”123456”和第6行的字符串”abcdef”,分别用于初始化形如buf[]和buf2[]这样的字符数组,在C语言中,这是一种初始化数组的特殊用法,对此类字符串的语义检查,会在declchk.c的CheckInitializerInternal()函数中,即在对数组初始化进行检查时一并处理,而不会调用exprchk.c中的CheckPrimaryExpression()函数。上述hello.c中的其他字符串,都不是用于初始化形如buf[]的字符数组, UCC编译器会调用CheckPrimaryExpression()函数为这些字符串命名。对C程序员而言,这些字符串就相当于是“匿名的”字符数组,如图4.2.8第15至17行所示。 接下来,我们就可以来分析一下对基本表达式进行语义检查的函数CheckPrimaryExpression,如图4.2.9所示。对于形如123这样的常量,在语法分析时我们就已经在其语法树结点中建立了其类型信息,在语义检查时,不需要做其他工作,所以由图4.2.9第5行直接返回。而对于字符串,UCC编译器会为之取一个形如.str1这样的名称,并且将字符串加入到一个链表中,以便在代码生成时,能生成如图4.2.8第15至18行那样的汇编代码,这个工作主要由图4.2.9第9行的AddString()函数来完成。被UCC编译器命名后的字符串,就相当于具有了标识符的语法地位,所以第8行将字符串对应语法树结点的op域改为OP_ID,在C语言中,我们还可以取字符串”abc”的地址,例如printf("%p\n",&"abc"),相当于字符串”abc”具有C程序员可见的内存地址,这意味着我们可将字符串当作左值来对待,因此第10行将相应结点的lvalue域置为1。
图4.2.9 CheckPrimaryExpression()
图4.2.9第13至33行用于处理形如var的标志符OP_ID,这需要先查一下符号表,看看标志符var是否已经声明过,如果是未声明就使用,则在第16行报错,而第17行通过函数AddVariable()往符号表里添加一个int型的变量,这是“将错就错”的策略,以便后续的语义检查能继续进行下去。如果通过typedef定义得来的类型名当作变量来使用,如第20行注释中的INT32 = 3所示,则在第21行进行报错。对于形如第23行注释中的枚举常量RED,则可以把它当作整数常量来处理,如第24至26行所示。对于其他的标识符,我们把从符号表中查找得来的类型信息复制到语法树结点上,如第28至30行所示。函数名和数组名不可充当左值,第31行设置了函数名结点的lvalue域为0,而对于数组名对应的语法树结点而言,语义检查时如果调用了Adjust()函数来做类型调整,则会在Adjust()函数中中置其lvalue域为0。例如,对于以下的表达式arr+1来说,在对二元运算符进行语义检查时,我们会调用Adjust()来把数组名对应的语法树结点的类型调整为int *,这样就可以进行arr+1的指针运算,而对于&arr而言,我们需要把arr当作左值来处理,这样可以取其地址,所以在对运算符&进行语义检查时,就不需要调用Adjust()函数。
int arr[4];
arr+1;
&arr;
我们现在来看一下图4.2.9第9行用到的AddString()函数,相应的代码如图4.2.10所示。我们要做的工作主要是为这些字符串取一个名称,第442行的FormatName()函数完成了这个工作,这是一个C语言的变参函数,我们后续会专门用一小节来介绍一下C语言变参函数的实现原理。
图4.2.10 AddString()
图4.2.10第439行创建了一个struct symol对象,第440至445行用于对这个符号对象进行初始化,第441行设置该符号为SK_String类别,第444行的TK_STATIC则意味着这些“无名的”字符串其实被UCC编译器视为static的字符数组。在UCC编译器中,字符串并没有被添加到符号表里,而是用一个由若干个struct symbol对象构成的单向链表来记录,ucl\symbol.c中的全局变量Symbol Strings记录了该链表的链首地址。第447行的StringTail始终指向这个链表的尾部,第447至448行完成了这个插入操作。
为了更清楚地对比语法分析后和语义检查后相关语法树结点的变化,我们给出了图4.2.11,其中描绘了标志符结点arr,常量结点3及索引结点arr[3]的对比情况。从中我们可以发现,op域为OP_CONST的常量结点在语义检查前后是没有发生变化的,而op域为OP_ID的标识符结点却发生了一些变化。语义检查时,通过查符号表,我们为图中右侧的arr结点的ty域添上了类型信息,在符号表中arr的类型为int [5],但经过我们前面介绍的Adjust()函数的类型调整后,arr结点的类型为int *,其ty域本应指向一个struct type对象,为了简单起见,我们直接在图中标上int *,其val域在语法分析后是指向字符串”arr”,但经语义检查后,我们让val指向标识符arr对应的符号对象,在后续阶段进行中间代码和汇编代码生成时,我们需要用到arr对应的symbol对象的内容。图中左侧的语法分析后的arr结点中,我们没有画出其isarray和ty等域的内容,这些内容缺省值都为0,因为我们在从UCC的堆空间创建一个对象时,已把这些对象的内容清0。
图4.2.11 语法树的变化示意图
需要特别说明的是,我们相当于是调用CheckExpression(arr[3])来进行语义检查,在CheckExpression()函数中,会按后序遍历的次序来对语法树进行检查,其原因是只有知道了arr对应结点的类型,我们才能知道arr[3]对应结点的类型。通过对声明int arr[5]的语义检查,我们建立了类型系统,这些类型信息是保存在符号表中。前面我们已介绍过,对声明的检查我们会在分析declchk.c代码时进行讨论。在对表达式arr[3]时行语义检查时,我们需要从符号表中检索出arr对应的类型信息,然后让这些类型信息自下而上地在语法树上传播。结点arr的op域为OP_ID,因此,我们是用CheckPrimaryExpression函数来对之进行语义检查的,按图4.2.9第31行所示,在该函数中arr结点的lvalue是先被置为1的,但是当CheckPrimaryExpression返回时,我们会回到CheckPostfixExpression()函数中,此是结合上下文,我们调用了Adjust()函数完成对左子树arr的类型调整,从而使arr结点的lvalue为0,即数组名arr不是左值,其类型为int *,同时把isarray置1,代表类型调整前arr结点为数组类型。由于arr结点的类型为int *,从而arr[3]结点的类型就为int,且arr[3]结点的lvalue域为1,表示该结点为左值。C编译器剖析_4.2 语义检查_表达式的语义检查(3)_字符串与标识符
原文地址:http://blog.csdn.net/sheisc/article/details/44035983