早年封存的编译器设计与实现的文稿,现已解封,以飨读者。(共上、中、下三篇)
编译器构造(中)
五、符号表
符号表是编译过程中保存程序信息的数据结构,它从语法分析模块获取所需的信息,为语义处理和代码生成模块服务。主要功能如下:
(1)保存变量、函数的信息记录。
(2)开辟串空间,保存静态字符串。
(3)管理局部变量的可见性。
(4)处理变量、函数的声明和定义。
5.1数据结构
符号表相关的数据结构如下:
1.变量记录数据结构定义如下:
structvar_record{symboltype;stringname;union{intintVal;charcharVal;intvoidVal;intstrValId;};intlocalAddr;intexterned;};
变量记录数据结构的字段的含义如下:
(1)type:记录变量的类型,值是枚举类型symbol的rsv_int、rsv_char、rsv_void、rsv_string
。
(2)name:
记录变量的名字。
(3)匿名联合类型:记录变量的初值,如果没有初值初始化为0,最关键的是strValId字段,它标志着字符串类型变量的存储位置。strValId为-2时表示字符串为全局定义的字符串,存储在数据段中;strValId为-1时表示字符串是局部定义的字符串或者是临时结果字符串,存储在堆栈段中;strValId为大于0的正整数时表示常量字符串存储在串空间的ID
。
(4)localAddr:表示局部变量的栈中位置相对于ebp的偏移量,若localAddr为0
表示改变量是全局变量。
(5)externded
:表示变量是否是外部变量。
2
.函数记录数据结构定义如下:structfun_record{symboltype;stringname;vectorsymbol*args;vectorvar_record**localvars;intdefined;intflushed;inthadret;voidaddarg();inthasname(stringid_name);voidpushlocalvar();intgetCurAddr();voidflushargs();voidpoplocalvars(intvarnum);intequal(fun_recordf);var_record*create_tmpvar(symboltype,int,int);};
函数记录数据结构的字段说明如下:
(1)type
:函数的返回类型,和变量记录相同。
(2)name
:函数名。
(3)args
:指向参数类型链表的指针。
(4)localvars
:指向局部变量记录链表的指针。
(5)defined
:指示函数是否定义。
(6)flushed
:指示函数的参数缓存的信息是否写入了符号表。
(7)hasret:指示函数在末尾是否有return
语句。函数记录数据结构的主要方法定义如下:
(1)addarg():
为函数头声明的时候将参数变量信息写入缓冲区。
(2)hasname(string):
测试在函数作用域内是否有参数指定名字的变量声明,包含参数名字。
(3)pushlcoalvar():
将局部变量的信息压入局部变量链表,并写入符号表。
(4)getCurAddr():取得当前分析代码时刻堆栈指针相对于ebp
的偏移。
(5)flushargs():
将参数缓存的参数信息写入符号表。
(6)poplocalvars(int):
从局部变量链表后边弹出参数指定数目的变量信息,同时在符号表删除变量信息。
(7)equal(fun_recordf):
判断参数指定的函数的声明是否和本记录的声明合法匹配。
(8)create_tmpvar(symboltype,inthasVal,intvar_num)
:为常量类型创建一个临时变量,参与表达式运算或者参数传递。
3
.符号表数据结构记录所有的符号信息,包括变量和函数符号的信息,另外还增加了一定的扩展信息,数据结构定义如下:classTable{hash_mapstring,var_record*,string_hashvar_map;hash_mapstring,fun_record*,string_hashfun_map;vectorstring*stringTable;vectorvar_record*real_args_list;public:intaddstring();stringgetstring(intindex);voidaddvar();voidaddvar(var_record*v_r);var_record*getVar(stringname);inthasname(stringid_name);voiddelvar(stringvar_name);voidaddfun();voidaddrealarg(var_record*arg,intvar_num);var_record*genCall(stringfname,intvar_num);voidover();voidclear();};
符号表数据结构的字段说明如下:
(1)var_map:
变量记录哈希表。
(2)fun_map:
函数记录哈希表。
(3)stringTable:
串空间。
(4)real_args_list:
函数调用实参变量记录链表。符号表数据结构的主要方法说明如下:
(1)addstring():向串空间添加一个常量串,id从0
自增。
(2)getstring():
根据串的索引获取串内容。
(3)addvar():
向符号表添加一个变量记录信息。
(4)getVar(string):
根据变量名字获取变量声明的记录信息。
(5)hasname(string):
测试指定的名字是否和当前作用域的变量的符号名重复,函数名称不需要测试。
(6)delvar(string):
删除指定名称的变量。
(7)addfun():
向函数记录哈希表添加一条函数记录,同时检查函数的声明和定义的合法性。
(8)addrealarg(var_record*arg,intvar_num)
:向实参列表中添加一个实参变量记录。
(9)genCall():
产生函数调用的代码。
(10)over():
产生数据段信息。
(11)clear():
清空符号表信息。
4
.全局对象
var_recordtvar
:记录当前分析的变量的声明定义信息。
fun_recordtfun
:记录当前分析的函数的声明定义信息。
Tabletable:符号表引用对象。
5.2局部变量作用域管理
局部变量作用域管理算法执行流程如图5-1所示:
图5-1局部变量作用域管理流程
可以看出,变量声明或者定义时,编译器获取变量类型和名称信息,修改相关字段的内容,然后将信息插入符号表。函数声明时,编译器先插入函数记录到符号表,然后对参数声明处理方式是:先把参数变量记录信息存储在局部变量列表缓存中,若检测出是函数定义再把缓存的变量记录信息真正的插入符号表,否则清空缓冲区。
函数定义时,编译器先将函数记录信息插入符号表,再将局部变量的定义依次插入符号表,并且记录函数内插入变量的个数,等到函数定义结束的时候将刚才插入的变量依次从符号表删除,最后清除缓冲区的变量记录,更新符号表。另外,在表达式解析的过程中会产生临时的局部变量,对其也当作正常的局部变量进行处理即可。
根据上述的变量处理规则,可以实现变量作用域的正确管理。根据5-2
这个实例可以更加清晰的看到这一点。由此可以得出结论:
(1
)全局变量登记后不会退出符号表。
(2
)局部变量记录在域结束后退出符号表。
(3
)临时变量同局部变量,但不能被程序直接访问。
(4
)域会对其内部声明的变量计数,以便结束时弹出其记录。
(5
)不同作用域的变量声明必然不能相互访问。
图5-2变量作用域管理实例
六、语义处理
语义处理作为语法分析的补充,能分析语法分析不能分析的语义信息,其主要功能如下:
(1
)引用符号表内容,检查语义的合法性。
(2
)引导代码生成例程。有了语法分析产生的符号表内容,语义处理可以通过查询符号表的信息来对已经声明的语法进行合法性的语义检查。当语义检查没有错误时就可以引导代码生成例程进行代码生成的工作。
所有的语义错误如表6-1
所示:
图6-1语义错误
下面结合这些错误分别对各类语义错误进行分析。
6.1变量、函数声明的合法性
extern关键字是对外部变量的声明。extern声明可以重复出现,以保证每个单独的文件都能引用别的文件的全局变量,对extern
变量可以只是声明但不使用。当出现变量定义时,语义模块先查询符号表是否含有该名称变量的变量记录信息,若没有则插入新的变量记录,否则说明变量已经定义了(不管是内部还是外部变量),都会报告语法错误,代码如下。voidTable::addvar(){if(synerr!=0)//有语法错误,不处理return;if(var_map.find(tvar.name)==var_map.end())//不存在重复记录{var_record*pvar=newvar_record(tvar);var_map[tvar.name]=pvar;//插入tvar信息到堆中}else//存在记录,看看是不是已经声明的外部变量{var_record*pvar=var_map[tvar.name];//刷新变量记录信息deletevar_map[tvar.name];var_map[tvar.name]=pvar;//插入tvar信息到堆中semerror(var_redef);}}
和变量声明处理方式类似,函数定义的语义检查做类似处理,不同的是函数检查的时候还要注意参数的合法性匹配,而且对于函数声明,需要函数定义进行替换声明记录,具体代码如下。
voidTable::addfun(){if(synerr!=0)//有语法错误,不处理return;if(fun_map.find(tfun.name)==fun_map.end())//不存在记录{fun_record*pfun=newfun_record(tfun);fun_map[tfun.name]=pfun;//插入tfun信息到堆中//函数定义生成代码if(pfun-defined==1){tfun.flushargs();genFunhead();}}else//函数声明过了{fun_record*pfun=fun_map[tfun.name];//取得之前声明的函数信息//验证函数声明if(pfun-equal(tfun)){//参数匹配,正常声明if(tfun.defined==1)//添加函数定义{if(pfun-defined==1)//已经定义了{//重复定义错误,覆盖原来的定义,防止后边的逻辑错误semerror(fun_redef);//函数形式完全相同,不需要更改函数记录,刷新参数即可tfun.flushargs();}else{//正式的函数定义pfun-defined=1;//标记函数定义tfun.flushargs()//函数定义生成函数头代码genFunhead();}}return;}else{//插入新的定义声明fun_record*pfun=newfun_record(tfun);deletefun_map[tfun.name];//删除旧的函数记录fun_map[tfun.name]=pfun;//插入tfun信息到堆中//参数声明不一致++if(tfun.defined==1)//定义和声明不一致{semerror(fun_def_err);tfun.flushargs();}else//多次声明不一致{semerror(fun_dec_err);}}}}
6.2break、continue语句的位置
根据语法规则,break和continue
语句只能出现在循环体内部,然而语法定义中把这两种语句作为正常语句处理,所以需要在语义处理中对他们的位置进行合法性检查。
在出现循环语句的时候,为该循环设置一个唯一的标识ID,将ID的引用传递给循环体的复合语句模块,即使出现循环嵌套,复合语句的也总能获得最内层的循环的ID。在复合语句中,若出现break或者continue语句时,检测该ID是否为0。若ID为0,说明没有循环语句为复合语句传递参数,报告语义错误;否则,接收的ID即循环体的ID,表示break或者continue语句合法,由于循环体生成代码时的标号名称为“
whileID”或者“whileID_exit”,所以,此时也为break语句和continue语句提供了跳转地址的信息。
6.3return语句返回值类型
根据语法规则,return语句可以出现在函数体的任何位置,在检测到return语句时,产生函数退出的代码。但是,在函数体内部可能会出现多层的复合语句,而在函数的第一级作用域内没有return
语句,从而导致函数生成的代码没有退出语句。
所以,为了保证程序的正常执行,必须在出现return语句的同时,检测作用域的级别,若为1则正常,否则就是内部复合语句的return,此时函数记录的hasret字段不能置为1。同时,还要return
语句返回值和函数定义的类型匹配,本系统要求它们严格匹配,不进行默认的转换。
另外,return
语句生成的代码中会强制恢复堆栈指针,因此不会导致程序堆栈空间崩溃。
6.4函数调用语句实参列表的合法性
在函数调用语句出现的时候,要对函数调用的实参表达式依次计算,得到表达式的类型,然后对该类型与函数的参数列表进行匹配,若成功则生成函数调用的代码,否则报错,具体代码如下。var_record*Table::genCall(stringfname,intvar_num){var_record*pRec=NULL;if(errorNum!=0)//有错误,不处理returnNULL;if(fun_map.find(fname)!=fun_map.end())//有函数声明,就可以调用{fun_record*pfun=fun_map[fname];//匹配函数的参数//实参列表是共用的,因此需要动态维护if(real_args_list.size()=pfun-args-size())//实参个数足够时{intl=real_args_list.size();intm=pfun-args-size();for(inti=l-1,j=m-1;j=0;i--,j--){if(real_args_list[i]-type!=(*(pfun-args))[j]){semerror(real_args_err);break;}else//匹配{}}//产生函数返回代码if(pfun-type!=rsv_void)//非void函数在函数返回时将eax的数据放到临时变量{pRec=tfun.create_tmpvar(pfun-type,0,var_num);//创建临时变量if(pfun-type==rsv_string)//返回的是临时string,必须拷贝{var_recordempstr;stringempname="";empstr.init(rsv_string,empname);pRec=genExp(empstr,addi,pRec,var_num);}}//清除实际参数列表while(m--)real_args_list.pop_back();}else{semerror(real_args_err);}}else{semerror(fun_undec);}returnpRec;}
6.5赋值语句的类型转换
赋值语句能进行默认类型的转换,所以在表达式处理过程中,要根据被赋值变量的类型将表达式的结果进行默认转换,如果默认转换不能进行则报错,这种默认转换过程在代码生成过程进行。
(未完待续)
赞赏
人赞赏
石家庄白癜风专科医院哪家白癜风能彻底治愈