(转载请注明出处 http://www.cnblogs.com/BlackWalnut/p/4527845.html)
写完语义分析的代码后感觉语义分析只是为了进行类型检测(后来才发现,这只是语义分析的一部分)。词法分析注重的是每个单词是否合法,以及这个单词属于语言中的哪些部分。语法分析的上下文无关文法注重的是一个一个的推导式,是将词法分析中得到的单词按照语法规则进行组合。那么,语义分析就是要了解各个推导式之间的关系是否合法,主要体现在推导式中使用的终结符和非终结符之间的关系,也就是它们的类型。所以语义分析注重的一个方面是类型检测。
为了将上下文无关文法中各种终结符和非终结符联系起来,以及在想要使用它们的时候得到它们相应的类型,我们使用了一种叫做符号表的东西,又称为环境。环境可以理解为对每一个ID建立一个栈,栈中存放的和这个ID相关的一些信息,这些信息称为绑定。使用栈的好处是,可以解决作用域的问题。ID始终对应于栈顶的绑定,每次进入一个新的作用域就将一个作用域标示符压入栈中。这样,在一个新的作用域中定义了一个和老作用域ID相同的变量,类型或者函数时,将新的绑定压入栈中,那么老的绑定就会失效。当出作用域时,将作用域标示符以上的所有绑定都弹出,这样就完成了老绑定的恢复。
上面介绍的栈式环境,我们称为命令式风格。还有一种称为函数式风格,它的特点是每次都将原来的环境复制一份,将老的保存起来,对新的进行更改。当出作用域时,直接将新的抛弃,然后使用老的。
为什么只存放变量,类型以及函数的相关信息呢?我们可以看到,一种语言其实包含四个部分类型声明,函数声明,变量声明以及表达式, 前三个就利用内置类型来创造新的东西,而表达式则是使用这些东西所要遵守的规则,这些规则都是由上下文无关文法定义好的。声明和表达式就组成了一个语言的基本部分,我们只能使用这些规则来组织我们创造的东西,最终形成我们的程序。所以在语义分析阶段,我们只用注重各种类型的检测,看看在特定的规则下是否符合要求。
下面我们来看看tiger中语义分析时要注意的事项。
在tiger的语义分析过程中,我们使用了两个环境,值环境和类型环境。其中,值环境用来存放函数声明,变量声明的,类型环境是用来存放类型声明的。之所以使用两个符号表是因为在tiger中我们允许类型和变量,函数名使用相同的ID,但是不允许变量和函数使用相同的ID。并且,在tiger中,为了更快的找到ID所对应的绑定,我们使用了hash表来存储每个ID对应的栈。但是这里有个问题要解决,就是hash可能会造成冲突。例如ID1和ID2同时hash到同一个表项中,那么这个表项对应的栈是谁的?这个时候,我们要在绑定中存放这个绑定对应的ID。我们只要从栈顶开始比较,找到第一个和我们hash的ID相同的绑定,就是我们需要的绑定。
但是,这样的话,每次查找hash表都要进行字符串的比较,是十分低效的。所以,tiger中symbol.h文件给了另一个hash表,这个hash表的作用是将一个ID映射到一个指针(就是在语法分析中使用函数S_symbol)。那么我们只要将这个指针和绑定压入栈总,比较这个指针就可以确定这个绑定是不是我需要查找的绑定。这是一个十分精巧的设计。
知道了如何根据ID查找相应的绑定,那么我们来看看这些绑定究竟是什么。
首先,我们向栈中压入绑定时,绑定其实使用一个void*类型的指针指向的,也就是说,我们不关心压入的绑定是什么,我们只关心压入绑定的地址以及压入到哪个表中(函数s_enter)。在读出(函数s_look)绑定的时候,我们只要把这个地址转换为我们需要的绑定(值绑定或者类型绑定)就可以了。
对于类型环境,每一个类型ID对应一个Ty_ty_结构体定义的绑定,这个Ty_ty_在Types.h中定义。可以看出这个结构体包含的类型(kind 枚举)是很多,其中要注意的是TY_name这个枚举,这个枚举是所有由 type id = id 这种语句定义的类型。根据不同的类型,我们使用联合中不同变量来解读。后面的几个函数都是根据不同kind来创建不同Ty_ty。但是要注意到,在新建一个类型环境的时候,我们要把int和string两个ID以及对应的绑定压入(S_enter)栈中,这两个是内置类型,必须先入栈。这里的int和string是类型,也就是终结符ID。和词法分析时的INT STRING的概念不同,这两个INT STRING是整型常量和字符型常量,它们的类型是int和string。
对于值环境,我们使用env.h中的E_enventry 结构体。这个就相对简单许多。
知道tiger中使用的绑定是什么样子的,就来说说这里面的一些坑。注意到我们使用到很多指针,大部分都是由指针引起的。
首先,我们在进行类型比较的时候,使用的指针。这里说说为什么,对于 type intary = array of int 以及 type strary = array of string ,如果只intary和strary所对应的绑定的kind来判断,两个都是Ty_array,但是两个的类型确实是不一样的,所以这种只使用kind判断的方式是失效的。所以这个时候我们要看看绑定中联合里的array(这是一个指针)是否一致。我们查看内置类型(int vaid nil sting),它们都是由函数(Ty_Int等)直接返回的指针,查看函数后发现,这些内置类型使用的都是相同的地址。但是其他类型的地址却有可能不同,这时就是下面这个注意事项。
另外一个需要注意的是,因为在tiger中存在这样的类型定义 type ID1 = ID2 也就是说ID1 是 ID2 的别名,此时ID1中的绑定指向由 Ty_Name 函数返回的一个地址,该函数申请了一块新的内存。如果我们再定义type ID3 = ID2 ,此时使用指针比较ID1 和 ID3,这个时候判断两个类型是不一致的。这显然和tiger的要求的相违背。这个时候我们定义了一个新的函数actrulyTy,这个函数将返回绑定的“真实类型”,这些真是类型只可能是四个内置类型 ,数组类型或者记录类型,同时书中要求返回的任何expty中的Ty_ty必须是“真实类型”。那么,对一下代码进行类型检测就可以得到正确的结果。
type recd = { a : int , b : string } type recd1 = recd type recd2 = recd //测试 recd1 和 recd2 是否相等 recd1 == recd2
这里提醒一下,我提供的代码实不支持一下类型检测的
type recd = { a : int , b : string } type recd1 = recd type recd2 = recd type recd1ary = array of recd1 type recd2ary = array of recd2
//以下此时将返回false
recd1ary == recd2ary
其实就是将recd1和recd2 再次进行一次actrulyTy就可以了。总之,两类型比较时,一定要求时“真实类型”。
还有一个,可能是虎书的作者没有注意到的一个地方(或许是我的代码有问题??)。在使用词法分析器向语法分析器传送ID以及相应的字符串时,我们使用一个变量(在我上一篇文章中讲述bison和flex传值)yylval.sval ,注意,这个sval是一个指针,指向一个字符串的开头,被指向的这个字符串是yytext。这个yytext字符串在进行词法分析时是会改变的。所以当你在语法分析器中将词法分析器传出的sval作为参数调用S_symbol时,这个sval指向的字符串yytext可能已经改变了(因为语法分析器存在移进以及规约,所以并不是和词法分析器同步工作的)。因此在使用s_symbol时要进行一些调整,如下:
S_symbol S_Symbol(string Id) { int i = 0 ; for ( ; (Id[i] >= '0' && Id[i] <= '9') || ( Id[i] >= 'a' && Id[i] <= 'z')||( Id[i] >= 'A' && Id[i] <= 'Z') ;++i) ; string name = (string)checked_malloc( sizeof(*name) * (i+1)) ; int b = sizeof(*name) * (i+1) ; memcpy(name , Id , sizeof(*name)*(i)) ; name[i] = '