编译原理 语义分析器

链接: 代码工程.

一、实验目的

1.学习已有编译器的经典语义分析源程序。
2.通过本次实验,加深对语义分析的理解,学会编制语义分析器。

二、实验任务

1.阅读已有编译器的经典语义分析源程序,并测试语义分析器的输出。
2.用C或JAVA语言编写一门语言的语义分析器。

三、实验内容

(一)学习经典的语义分析器

1.选择一个编译器

选择TINY编译器来进行语义分析器部分的学习。

2.阅读语义分析源程序并理解

对相关函数与重要变量的作用与功能进行稍微详细的描述。
2.1.相关文件
如图所示,语义分析部分相对于前面已经学习过的语法分析器多了四个文件,分别是analyze.c、analyze.h、symtab.c、symtab.h;
symtab.h、symtab.c文件是符号表的相关代码,作用是实现了一个分离的链式杂凑表结构的符号表;
analyze.h、analyze.c文件是符号表生成以及语义检查的相关代码,具体作用就是遍历语法分析生成的语法树将各个变量插入到符号表中,以及对抽象语法树进行类型检查;
在这里插入图片描述

接下来根据四个文件中的函数与变量进行详细的分析。

2.2.重要变量/结构
①LineListRec
在这里插入图片描述
动态分配链表,类型名为LineList,存储记录在杂凑表中每个标识符记录的相关行号(一个变量仅有一个定义但是会在多行出现)。

②BucketListRec
在这里插入图片描述
“桶”列表记录标识符信息,包括名字、在代码中出现的行号链表(用上述的LineList记录)、第一次定义的位置、下一个桶的指针,类型名为BucketList。

③hashTable
哈希表,也就是我们要求的符号表,表结构就是上述的BucketList;
在这里插入图片描述

2.3.相关函数
①hash
在这里插入图片描述
通过标识符的名字字符串来计算将该标识符放入hashTable中的哪一个桶链表中,加上hash函数可以将标识符平均的分配到各个桶链表中,加快后面运行过程中的查询速度。
②st_insert
在这里插入图片描述
该函数的作用很直观,将存储位置位于loc、行号为lineno、名字为name的标识符加入到哈希表中,如果已经存在则将行号加入到相应的LineList中,否则需要新开辟一个桶。

③st_lookup
在这里插入图片描述
该函数就是为了查找标识符的位置,也就是桶结构中的memloc,未找到则返回-1,用于语义分析中的类型检查。

④insertNode
在这里插入图片描述
该函数非常重要,是建立符号表的主要函数它根据抽象语法树的结点类型调用st_insert来将标识符插入到符号表中。

⑤buildSymtab
在这里插入图片描述
顾名思义,该函数就是建立符号表的入口函数,它会通过前序遍历的方式遍历所有树结点并且调用insertNode将所有标识符加入到符号表中。
⑥checkNode
在这里插入图片描述
该函数是类型检查的主要函数,也也是根据标识符号的类型检查是否有错误出现,并将标识符的计算结果的类型层层上传。
⑦typeCheck
在这里插入图片描述
与bulidSymtab相似的实现方式,是类型检查的入口函数。

⑧traverse
在这里插入图片描述
该函数较为重要,因此详细的进行解释:
这里的参数是两个函数指针,分别进行前序遍历的处理部分以及后序遍历的处理部分;也就是说我们的前序遍历和后序遍历都可以通过调用该函数实现,只是在调用时,需要考虑前序遍历和后序遍历的处理函数,比如对于前序遍历的处理函数指针preProc,为了通过前序遍历实现符号表的构建,我们需要将上述的insertNode作为函数指针传递进去;

3.理解符号表的定义

理解符号表(栏目设置)与基于抽象语法树的类型检查/推论的实现方法(树遍历)。
3.1.树遍历的实现方法
为强调标准的树遍历技术,实现buildSymtab和typeCheck使用了相同的通用遍历函数 traverse(上面已有他的介绍),它接受两个作为参数的过程(和语法树),一个完成每个节点的前序处理,一个进行后序处理;
给定这个过程,为得到一次前序遍历,当传递一个“什么都不做”的过程作为 p r e p r o c
时,需要说明一个过程提供前序处理并把它作为 p r e p r o c传递到t r a v e r s e。对于T I N Y符号表的情况,前序处理器就是 i n s e r t N o d e,因为它完成插入到符号表的操作。“什么都不做”的过程称作n u l l P r o c,它用一个空的过程体说明 。然后建立符号表的前序遍历由b u i l d S y m t a b过程内的单个调用:**traverse (syntaxTree, insertNode, nullProc);**完成。
类似地,t y p e C h e c k要求的后序遍历由单个调用traverse (syntaxTree, nullProc, checkNode);完成。
事实上,有了这样的标准的树遍历方法traverse之后我们是可以只使用一个traverse (syntaxTree, insertNode, checkNode) 就可以实现符号表的建立以及类型检查了,这得益于TINY不需要检查变量的定义,否则需要首先建立符号表。

3.2.符号表的定义
从上述的重要结构声明其实很容易就可以得到符号表的详细结构,我将它用图画出来表示为:
在这里插入图片描述

4.测试语义分析器

对TINY语言要求输出测试程序的符号表与测试结果。
4.1.修改主函数,使得TINY编译器进行语义分析;
在这里插入图片描述
4.2.测试用例一:sample.tny。
样例与测试结果:
在这里插入图片描述
此样例是多次使用的指导书上的样例,语法分析输出结果正确(共x、fact两个变量,x在5、6、9、10、10、11行出现、fact在7、9、9、12行出现)。
4.3.测试用例二
用TINY语言自编一个程序计算任意两个正整数的最大公约数与最大公倍数。
自己编写的最大公约数与最大公倍数的程序:
因为没有%求余符号,只能通过下面的乘法与除法结合的方式实现求余。
此外,不知道是不是因为提供的TINY的bug的原因,end、until的前面的语句不能有分号,且程序最后一行也不能有分号!
在这里插入图片描述
在这里插入图片描述

测试结果:在这里插入图片描述

(二)实现一门语言的语义分析器

1.语言确定:
C-语言,其定义在《编译原理及实践》附录A中。
2.C-语言的语义分析器具体设计见下面的系统设计
3.具体代码见源程序。

四、系统设计(C-语言的语义分析器)

1.完成C-语言的符号表的定义设计。规划类型检查/推论的实现方法。

1.1.文件结构

在这里插入图片描述
前面的语法分析实验,扩展语义分析部分的功能,将符号表的相关结构定义全部定义在了Globals.h中,语义分析部分功能的实现都写在了analyse.cpp中,而analyse.h头文件中仅含有语义分析函数的声明void Semantic_parse(TreeNode* syntaxTree);
从文件可以看出,我是通过unbuntu实现的语义分析器,并且通过编写makefile文件来进行工程的编译链接。

1.2.重要数据结构(符号表的设计)

这里将主要的符号表相关数据都定义在Globals.h中,下面逐一进行分析:
①函数存储结构
在这里插入图片描述

首先是函数在符号表中的数据结构,即存储定义函数的参数个数、各个参数的类型(通过vector实现),函数返回的类型、函数的栈大小(用于后面实验的代码生成)

②变量存储结构
在这里插入图片描述
此处表示变量在符号表中的存储结构,其中包括在源码中的行数(仅存储定义的位置)、在内存中分配的位置(如果表示的是函数参数,那么这里表示参数的位置,即第几个参数)、作用域(0表示全局域、1表示函数参数、2表示函数体、接下来则是根据while或者if的{}来决定作用域、每多加深嵌套一层则scope加1)、变量类型、大小。
作用域的划分如上右图!

③同名变量在不同作用域中的定义实现
在这里插入图片描述
这里使用一个vector来存储同函数中同名变量(作用域不同),比如在while循环中定 义了与参数同名的变量。或者如上右图中在while循环中定义了一个同名变量a,在我 的代码实现中,就是存储在了vectorLines中;

1.3.符号表的组成

在这里插入图片描述
这里是最重要的符号表实现部分,上面的三个结构体只是符号表的相关存储结构;
可以很明显的看到,我是使用了map的数据结构来进行符号表的相关映射的;
其中共有两个符号表,分别是函数的符号表以及各个函数中变量的符号表,这里的函数符号表的string代表函数的名字,即通过函数名字来进行映射;而变量的符号表比较复杂,首先通过函数名映射到函数中变量的符号表,接着再通过变量名映射到变量的相关信息结构(即第一个string是函数名称、第二个string是指变量名称)。
这里没有专门定义全局变量的符号表,因此直接将全局也看成一个函数,即”GLOBALS”表示全局变量的“函数”名,我们访问全局变量,可以通过以下方式:sys_table[“GLOBALS”][“变量名”]. line实现。

1.4.符号表图示

无法清楚描述符号表的组成,因此画出一个图示(通过wps流程图绘制):
在这里插入图片描述
通过这张图示就可以比较清晰的理解我的符号表的组成了。

1.5.简单规划设计

在TINY中直接使用前序遍历进行符号表的构建、后序遍历实现类型的检查,但是在C-中因为加入了函数、while等等可以增加新的作用域的语法,因此这里在进行符号表的构建时需要加上scope的栈式结构,即进入一个作用域时scope++,入栈,退出该作用域时直接弹出栈顶元素。
语义分析主要是要生成符号表,为各个变量分配好栈中地址,计算函数栈的大小。
使用变量时要找出正确作用域中声明的变量,因为可能存在同一名字的变量在不同的作用域被声明,这是重点也是难点。为此,可以使用一个栈来保存当前可以访问的所有作用域,每进入一个花括号,作用域加一,同时将该作用域压栈,退出花括号时,栈顶作用域出栈。全局域的作用域为0,函数的作用域从1开始,函数参数中的作用域为1,以此递增。退出函数时,作用域归0。这样,栈顶作用域为当前作用域,栈中的所有作用域为当前可以访问的作用域,在这些作用域中寻找最近声明的变量作为使用的变量。
作用域栈的表示如下:
在这里插入图片描述

栈的实现方式:
在这里插入图片描述
通过vector来实现,入栈则push_back,出栈则是pop_back,查看栈顶元素则是back();
也因此在实现前序/后序遍历的过程中,需要加入scope的入栈出栈过程,确保正确建立符号表以及类型检查:
在这里插入图片描述
在进行前序处理preProc之前,首先根据当前节点的类型进行判断,如果出现了COM_SK,即{号,那么代表进入了一个新的作用域,此时需要入栈;
在这里插入图片描述
在进行后序处理PostProc之后,需要根据结点的类型进行出栈的操作;

2.仿照前面学习的语义分析器,编写选定语言的语义分析器。

通过1.5简单规划中的分析,代码实现部分已经非常明确了,下面对于各个函数,进行简单的分析;

2.1.主函数main

首先通过语法分析函数生成抽象语法树tree,接着将tree作为参数调用语义分析函数,最关键的语句如下:

TreeNode* tree;
tree=Syntax_parse();
Semantic_parse(tree);

2.2.Semantic_parse语义分析入口

首先调用函数符号表的初始化函数,接着就进行符号表建立和类型检查的buildSymtab()、typeCheck()函数,关键语句如下:

fun_table_init();
buildSymtab(syntaxTree);
typeCheck(syntaxTree);

2.3.fun_table_init()

这个函数就是上面调用的函数符号表初始化函数,它所做的就是简单的将C-语言的输入输出函数input、output加入到函数符号表中。
这里如果不将output、input加入到函数符号表的话,将会产生很明显的问题,即无法使用input、output(未定义)。
因为无论是函数符号表还是函数对应的变量符号表,都是通过map来进行名字与表结构的映射的,因此这里加入的方法很简单;
①对input、output的函数表结构进行赋值
前者参数个数为0,后者为1、返回类型前者为int、后者为void,以input为例:

fun_table["input"].p_num=0;
fun_table["input"].return_type=Integer;
fun_table["input"].p_type.clear();		//简单的映射

②初始化Globals(将全局信息也作为一个函数看待)对应的变量表(函数是Globals中的变量看待)
作用域为0、input设置类型为Integer、output设置为Void、栈内存位置设置为-1(因为并不需要占用栈)、行号设置为0(并没有明确的定义input、output,将其看作是库函数),以input为例:

LineList l;
	l.scope=0;	l.ty=Integer; 	l.sizes=0;		l.linepoc=0;	l.loc=-1;
	sys_table["GLOBALS"]["input"].lines.push_back(l);

通过映射将Globals全局“函数”中的input标识符的信息l加入到变量表;

2.4.buildSymtab、typeCheck

调用这两个函数实现符号表的构建、类型的检查,无论是哪一个都是通过调用遍历语法树的traverse函数实现的(与TINY语音一样),这里的调用方法也与TINY一致:

traverse(syntaxTree,insertNode,nullProc);		//符号表的构建
traverse(syntaxTree,nullProc,checkNode);		//类型的检查

traverse函数与TINY不同的点在上面进行简单分析的时候已经确定了,需要进行作用域栈的维护,入{则入栈、出}则出栈;
但是在调用之前需要做好准备工作,主要就是作用域栈的初始化,将表示全局的scope=0入栈:

scope=0;
scopes.clear();
scopes.push_back(scope);

2.5.insertNode

traverse遍历函数通过传入函数指针的方式将inserNode函数作为遍历过程中的前序遍历的处理函数,即向符号表中插入结点;
插入结点到符号表的操作比较简单,根据结点的类型(主要是定义的)与符号表的组成进行插入即可,此外插入函数还需要判断当前使用的ID是否是定义过的(符号表中是否已经存在),这个过程在前序遍历中实现较为简单;
因为含有两个表,函数符号表以及对应的变量符号表,因此对不同的定义节点需要插入到不同的表(函数定义部分对应插入函数符号表、变量定义部分插入变量符号表);

以变量声明结点为例进行分析:
在这里插入图片描述
首先,如果当前作用域为0,那么需要手动将当前函数名替换为GLOBALS(因为这是我们假设的函数),将loc赋值为gloloc,gloloc是存储全局内存使用情况的(描述可能不清晰,简单来讲如果此前已经定义过一个int变量了,它需要4个内存位置,因此此时再为变量分配内存的话就需要从4开始)。
接着获得当前加入的变量的名字;
在这里插入图片描述
这一部分是检查是否出现重定义,具体做法是:
①判断同名变量表是否为空,非空得到该Var_sym;
②在Var_sym中遍历所有的该变量名的定义,查看是否有相同作用域的定义
③有的话那就是重定义、没有则进入下面的插入操作;
注意:重定义的检查只能出现在插入结点过程中(即前序遍历),因为作用域栈的关系,只能插入前检查。

在这里插入图片描述
前面已经完成了相同作用域的重定义检查,如果未冲突,那么进行以上插入操作;
具体来说就是讲变量的相关信息赋值到LinList结构变量中,然后将该LineList加入到该函数中该变量名对应的Var_sym中;
①对LineList进行赋值,包括所处的是scope、loc、sizes、ty、linepoc
②如果是全局变量,那么在将loc修改后应该保存到gloloc中备用;
③通过映射的方式将LineList存入sys_table(即最后一个语句)。

变量的声明部分就结束了,此外还有函数声明、变量使用、return、CALLK等结点的相关处理;这里需要对变量的使用IDK结点进行检查,即是否已经定义过,若没有则报错、对RETURN的返回值进行检查、对函数调用CALLK结点检查参数个数是否正确等;
以上检查也只能通过前序遍历进行(前序遍历过程中维护的作用域栈的原因),具体实现见源码;

2.6.checkNode

traverse遍历函数通过传入函数指针的方式将checkNode函数作为遍历过程中的后序遍历的处理函数,即类型检查;
类型的检查函数与TINY语言的检查函数checkNode类似,但是需要扩展很多新的结点类型,大致上检查返回、赋值、运算等等类型即可。

对于扩展的CALLK结点的类型检查分析如下:
在这里插入图片描述
首先获得该节点的函数名字,然后通过名字从函数符号表中得到该函数的返回类型;
再得到表示参数的结点,即child[1];
在这里插入图片描述
如果参数不为空,那么我们需要进行参数类型的判断,这里先得到所有实参的类型temp->type,存入vector tv1当中,对参数的遍历使用了while循环的方式;
在这里插入图片描述
形参的参数类型保存在函数符号表的p_type中,这也是一个vector结构,接下来将实参的类型与其比较,不同则说明出现问题(还可以检查参数个数是否正确);

至此CALLK的检查就结束了,其余的还有计算方面的检查(两个运算数类型是否相同)、赋值语句的检查、数组下标的检查等等,具体见源码;

3.准备2~3个测试用例,测试并解释程序的运行结果。

3.1.test.cm

这里的测试样例使用C-指导书中的样例(即最大公约数);
在这里插入图片描述
输出如下:
在这里插入图片描述
scope表示作用域、size表示大小、location表示栈空间、linepoc表示定义的位置、type表示类型;
上面也提到过将全局看成一个函数、全局中的函数定义并不占空间、参数列表中的location表示第几个参数;下面几行为各个函数需要的栈空间、这里只有main函数的x、y定义需要占用栈空间、每个大小都是4,因此总的大小需要是2;
因此具体来看输出正确;

3.2.test1.cm

在这里插入图片描述
为了详细的展示作用域以及var_sym的作用,用该例子进行分析;
这里在全局(scope=0)定义了x、y,接下来在main函数中(scope=2,scope=1表示参数)定义了x、y,然后在while循环中(scope=3)再次定义x、y,接着在while中嵌套while(scope=4),并且定义x、y;
我们查看输出结果:
在这里插入图片描述
可以看出在main函数中有三对x、y的定义,它们位于不同的作用域因此并不会报错,并且在栈中的位置是分配好的,main函数的栈大小共24(6个int变量);
对于函数main的x这一个名字的Var_sym就用vector存储了所有的三个不同作用域中x的定义。

3.3.test2.tm

下面是一些错误样例:
① 类型错误
在这里插入图片描述
output的返回类型是void,这里用于赋值会报错(符号表可以正确建立,但类型检查会报错)
结果如下:
在这里插入图片描述
在第5行报错,这里因为不支持中文的原因出现乱码。

② 未定义
在这里插入图片描述
未定义直接使用,会报错;
结果如下:
在这里插入图片描述
③ 重复定义
在这里插入图片描述
重复定义错误,同样报错;
报错如下:
在这里插入图片描述

五、实验总结

实验到此就结束了,这次实验非常的有难度,但也带来了很大的收获。
从学习TINY的符号表设计、语义分析的实现,到设计自己的符号表结构、语义的分析部分,每一步都学习到了很多新东西,尤其是当前作用域栈的保存部分是参考了非常多的源码后逐渐实现的。因为C-比TINY复杂的多,它的作用域分析算是其中的难点与重点部分了,因此刚开始是想到了课堂上讲解的方法,把符号表整个入栈,但是后面仔细思考发现我们需要保留一整个符号表,但是入栈出栈的操作势必损失一定的符号表(用完就删了),对符号表的保存比较麻烦,这里借鉴C-语法分析的资料后参考实现了这种作用域栈与符号表分离的方式。
这次实验最大的体会就是,阅读代码的能力也很重要!!一开始没有明确的思路,于是上github搜集了很多资料,也看过老师给的资料。查阅了现有的一些C-编译器的源码。与此同时我也发现了,书上学的和实际用的并没有什么很大的关联,有的只是思路,等到真正实践的时候还是要自己对理论进行适当的修改。
遗憾的是这次代码实现做的比较粗糙,尤其是类型检查非常的宽松,只能说勉强实现了一些功能,很多情况并没有检查到,但是也让我们认识了编译器的符号表以及类型检查的方法。

  • 4
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
这个里面的都是测试数据,总共得分5分。从控制台输入,不能从文件中读取。实现了基本功能,加分项目都没有去实现,没有函数数组这些的实现。这是用C++语言写的,新建parser类别要选C++,其他对于VS的配置和C语言一样。for语句用的是枚举所有情况,你可以自行修改。 对预备工作中自然语言描述的简化C编译的语言特性的语法,设计上下文无关文法进行描述 借助Yacc工具实现语法分析 考虑语法树的构造: 1.语法树数据结构的设计:节点类型的设定,不同类型节点应保存哪些信息,多叉树的实现方式 2.实现辅助函数,完成节点创建、树创建等功能 3.利用辅助函数,修改上下文无关文法,设计翻译模式 4.修改Yacc程序,实现能构造语法树的分析 考虑符号表处理的扩充 1.完成语法分析后,符号表项应增加哪些标识符的属性,保存语法分析的结果 2.如何扩充符号表数据结构,Yacc程序如何与Lex程序交互,正确填写符号表项 以一个简单的C源程序验证你的语法分析,可以文本方式输出语法树结构,以节点编号输出父子关系,来验证分析的正确性,如下例: main() { int a, b; if (a == 0) a = b + 1; } 可能的输出为: 0 : Type Specifier, integer, Children: 1 : ID Declaration, symbol: a Children: 2 : ID Declaration, symbol: b Children: 3 : Var Declaration, Children: 0 1 2 4 : ID Declaration, symbol: a Children: 5 : Const Declaration, value:0, Children: 6 : Expr, op: ==, Children: 4 5 7 : ID Declaration, symbol: a Children: 8 : ID Declaration, symbol: b Children: 9 : Const Declaration, value:1, Children: 10: Expr, op: +, Children: 8 9 11: Expr, op: =, Children: 7 10 12: if statement, Children: 6 11 13: compound statement, Children: 3 12

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值