学习已有编译器的经典语义分析源程序。
阅读已有编译器的经典语义分析源程序,并测试语义分析器的输出。
(1)选择一个编译器,如:TINY或PL/0,其它编译器也可(需自备源代码)。
(2)阅读语义分析源程序,加上你自己的理解。尤其要求对相关函数与重要变量的作用与功能进行稍微详细的描述。若能加上学习心得则更好。TINY语言请参考《编译原理及实践》第6.5节;PL/0语言请参考相关实现文档。
(3)理解符号表的定义(栏目设置)与基于抽象语法树的类型检查/推论的实现方法(树遍历)。
(4)测试语义分析器。对TINY语言要求输出测试程序的符号表与测试结果。对PL/0语言要求给出测试程序的各种符号表的内容。
TINY语言:
测试用例一:sample.tny。
测试用例二:用TINY语言自编一个程序计算任意两个正整数的最大公约数与最大公倍数。
PL/0语言:
测试用例一~三:test.pls,test2.pls,a1.pls。
通过本次实验,加深对语义分析的理解,学会编制语义分析器。
用C或JAVA语言编写一门语言的语义分析器。
(1)语言确定:C-语言,其定义在《编译原理及实践》附录A中。也可选择其它语言,不过要有该语言的详细定义(可仿照C-语言)。一旦选定,不能更改,因为要在以后继续实现编译器的其它部分。鼓励自己定义一门语言。
(2)完成C-语言的符号表的定义设计。规划类型检查/推论的实现方法。
(3)仿照前面学习的语义分析器,编写选定语言的语义分析器。
(4)准备2~3个测试用例,测试并解释程序的运行结果。
因在2018-2019秋季学期中,湖南大学编译原理实验6和实验7(语义分析和代码生成)首次变成了必做实验(之前是选做,几乎没有学长学姐选),难度过大,这里的代码借鉴了git上的一些,不完全本人原创。
https://pan.baidu.com/s/1FToQNdN0Li-R_-UiZPuLvQ
密码:txkz
注意,这一份代码运行在Linux系统下,作者的是Ubuntu 14.04。
运行方法:首先在文件夹下执行make,然后会生成一个compiler文件,那个就是C-的编译器,执行命令:
./compiler -a -f 文件名
即可完成其编译,当然包括语义分析。
TINY语言的语义分析器在loucomp文件夹下的analyze.c和symtab.c中,其对应的主要功能,analyse是用于语义分析本身,而symtab则是用于生成其对应的符号表。
进入语义分析部分的代码,则在MAIN.c中:
#if !NO_ANALYZE
if (! Error)
{ if (TraceAnalyze) fprintf(listing,"\nBuilding Symbol Table...\n");
buildSymtab(syntaxTree);
if (TraceAnalyze) fprintf(listing,"\nChecking Types...\n");
typeCheck(syntaxTree);
if (TraceAnalyze) fprintf(listing,"\nType Checking Finished\n");
}
其中,如果要进入语义分析阶段并有分析输出,则需要设置NO ANALYZE标记位为1,这样主函数才能够调用到build_Symtab和typeCheck函数,从而完成语义分析的工作。
下面,我们来分析语义分析器的输入输出,语义分析属于编译器前端的最后一部分,在此之前,编译器最开始的输入是一段代码,经过词法分析,输出的是词法单元,从而被语法分析单元所识别;语法分析的输入是一个个词法单元,通过分析这些词法单元之间的逻辑,利用递归下降等方法,形成一棵语法树,并将语法树的根结点存储在一个TreeNode类中,从而通过根结点就可以实现对于整个语法树的遍历(一般是前序遍历);之后,来到了语义分析部分,语义分析的输入是一个语法树,这里我们的输入是根结点;语义分析的输出,则是符号表和语法报错信息。
符号表的意义在于,分析代码中所有的声明,比如变量函数等内容;而语法报错信息,则会通过语法树结点关系,检测相邻词法单元是否符合文法规则:比如,int 1和int a两种输入,在语法分析阶段均可通过,但是在语义分析阶段,int 1会被识别为一个错误,因为根据语法规则,int是一个声明,声明后面只能跟着一个变量名ID,而词法单元1的属性是NUM,int后面是不允许接着一个NUM的。这样的实现例子还有很多,具体需要看语言本身的定义。
在本次实验的第二个任务中,要求实现一个C-语言的语义分析器,我们采用的是增量编程的方法,在TINY编译器上,结合C-的词法和语法特点,利用TINY编译器的现有构架,在此基础之上,完成C-的语义分析器。
while (getToken() != ENDOFFILE)
{
/* do nothing */
}; //词法分析部分
#else
fprintf(listing, "*** Parsing source program...\n");
syntaxTree = Parse(); //调用PARSE函数进行语法分析
/* Tracing enabled? Let's have it... */
if (TraceParse)
{
fprintf(listing, "*** Dumping syntax tree\n");
printTree(syntaxTree);
};
#if !NO_ANALYSE
if (!Error) //开始进行语义分析
{
fprintf(listing, "*** Building symbol table...\n");
buildSymbolTable(syntaxTree);
fprintf(listing, "*** Performing type checking...\n");
typeCheck(syntaxTree);
}
如上主函数部分,C-的语义分析和TINY一样,首先需要经过词法分析部分,之后声明一个语法树结点syntaxTree,对于该结点调用语法分析函数Parse,这样,语法树的根结点就被存储在syntaxTree这个根结点中,对它进行遍历就可以访问整个语法树;之后,也是需要将标记位ANALYSE设置为1,这样,会进入到语义分析的部分。
在语义分析部分,我们调用了两个函数,一个是buildSymbolTable,这个函数用于构建符号表;另外一个是typeCheck,这个函数用于遍历语法树,检测词法单元之间的逻辑关系是否符合语法规则,然后输出语法错误的原因和出错地点(行数)。
buildSymboltable这个函数在ANALYSE.C中,其定义如下:
void buildSymbolTable(TreeNode *syntaxTree)
{
/* Format headings */
if (TraceAnalyse)
{
drawRuler(listing, "");
fprintf(listing,"Scope Identifier Line Is a Symbol type\n");
fprintf(listing,"depth Decl. parm?\n");
fprintf(listing,"--------------------------------------------------\n");
}
declarePredefines(); /* make input() and output() visible in globals */
buildSymbolTable2(syntaxTree);
/* set the isGlobal field in appropriate nodes: needed in code generator */
markGlobals(syntaxTree);
/* Dump the global scope, if it's asked for */
if (TraceAnalyse)
{
drawRuler(listing, "GLOBALS");
dumpCurrentScope();
drawRuler(listing, "");
fprintf(listing, "*** Symbol table dump complete\n");
}
}
可以看出,该函数主要有四个步骤:
(1)该函数调用了drawruler函数,用于确定符号表的格式,为后期打印符号表做好准备。该函数的主要功能,是控制符号表的输出规模,规定一个统一的格式。
(2)该函数调用了declarePredefines函数做了一个输入输出声明的预处理工作:
该函数主要是为C-语言的I/O操作,即全局变量中的input函数和output函数创建符号表结点,并将其插入符号表。通过我们阅读C-语言的语法定义,可以了解到,一个标准的C-语言程序需要有输入和输出,而这样的程序IO操作即是通过input和output函数实现的,因此,C-程序中一定包含着input和output,因此,在预处理阶段,我们先将其加入到符号表中。
以上两个环节,均为预处理阶段,下面则是正式的符号表生成操作:
(3)该函数调用了buildSymboltable2函数,用于正式建立符号表。
可以看出,该函数的输入是语法树根结点,即我们需要遍历整个语法树,来寻找何种词法单元结点,需要插入到符号表中。
接下来涉及到了符号表的概念,符号表是整个代码中,所有声明过的变量和函数的记录者。比如在C-语言中,变量的声明只有一种,那就是INT,而函数的声明有两种,INT和VOID。
因此,我们得到了一个结论,那就是只有结点类型为DECK(声明结点)这样的结点,才需要插入符号表中。在上一步语法分析中,我们已经得出了各个结点的类型,因此我们一边遍历语法树,一边判断当前遍历到的结点是不是属于一个声明:
如果这个结点的类型是声明,那么就需要插入到符号表;而声明也分为两种,一种是变量的声明,一种是函数的声明;变量的声明比较好处理,直接插入到符号表中即可;而函数的声明处理,规则稍微多一些,因为函数的内部,也可以有一系列的变量声明,此时,如果结点检测到它自己输入声明结点,且声明类型为函数,那么就会调用之前提到过的drawruler函数,增加一张符号表,用于处理该函数内部的变量声明。
对于非声明类结点,只需要略过即可,但是需要进行错误检测。
这里调用了一个lookupSymbol函数,去查找当前结点的名字,这是因为,在其他语法树结点中,往往包含许多子结点,共同来实现一个功能,比如一个运算语句a+=1;这个语句中,a是否有被定义过呢?答案是不确定的,此时我们需要对于该结点的名字,也就是a,去到符号表中查找一下,是否存在之前某个地方定义过a,a是否已经在符号表中,如果a在之前没有被定义,说明这个语句是错误的,因为它应用了一个没有定义的词法单元,故可以输出错误信息。
(4)该函数最后调用了dumpCurrentScope函数,将符号表打印出来。
以上是对于建立符号表函数的分析。
那么符号表是如何工作的呢?这里涉及到第三步中调用的insertSymbol函数:
待插入符号表的信息,包括声明的变量的名字,该变量对应的树节点和该变量所出现的行数,其中行数的信息,已经在词法分析中完成存储了。
insert操作,主要是两部分,第一步已经在上图中展示。它调用了一个symbol already declared函数,确认检测一下是否有同名变量的重复声明,如果该待插入符号表的变量名字,和已经在符号表中的一个元素名称相同,那么就会报错,比如在同一个函数中,不能重复声明两个一样的变量。
第二部分则是通过了合法性判断,确认了该函数中无重复变量声明,因此就可以插入到符号表中,具体是通过数组模拟了一个哈希表,将符号的相关信息,比如名字,是否为函数的参数、以及出现的行数,记录到表中。
至此,符号表建立函数分析完毕。
在建立完符号表之后,语义分析的下一个任务是对于语法错误的检查,该部分的实现原理是,通过检查相邻语法树结点的词法属性,确保是符合常规的。
具体的实现函数在ANALYSE.C中的type check函数:
type check直接调用了一个遍历函数traverse,传入的三个参数,第一个是syntaxTree,代表语法树,第二个nullProc如下图,它是一个函数,代表遍历的终止条件,即遍历到叶子结点时,语法树遍历停止,第三个函数则是需要重点分析的checkNode,它制定了语法规则分析的过程。
上图展示的是遍历结束条件nullProc。遍历到语法树的叶子结点即结束。
下面是语法分析的规则函数check NODE:
【提醒一下,语法分析这里的截图不全,因为结点类型太多了,可以对照上面链接的代码阅读】
check node函数的工作原理是,一边遍历语法树,遍历到当前的一个结点之后,检查该结点的相邻结点是否符合语法规则,如果不符合就报错,如果符合就可以继续。
上图展示的是声明结点的处理规则:如果该节点是声明结点,那么它有三种情况,第一种是声明了一个数字,那么它的下一个结点必须是数字;如果声明的是函数,那么结点类型必须是函数声明;如果声明的是数组,它的声明类型必须也是数组,这样对应起来。
类似的例子还有很多。
下面再举一个if语句的例子:上图中,语法树的结点是if,代表条件判断,其结点中必须要有数字,否则不符合if条件判断的规则,就会报错。
其他的语句判断法则,根据C-语言的语法详细定义,由于该函数输入的是结点的语法类型,因此需要对于每种语法类型进行一一构造判断条件即可完成这一部分的内容。如果不符合条件,那么就会报错。这里通过的是switch语句进行条件判断,因此很容易确认程序中的语法犯了一个什么样的错误;另外,由于词法分析中已经确定了各个词法单元所出现在的行数,因此也能够很容易的定位到错误发生的位置。
至此分析完毕,语义分析的过程结束。
//A program to perform Euclid's algorithm to computer
//the greatest common denominator (GCD) of a pair of integers.
int gcd(int u, int v)
{
if (v == 0) return u ;
else return gcd(v,u-u/v*v);
/* u-u/v*v == u mod v */
}
void main(void)
{ int x; int y;
x = input(); y = input();
output(gcd(x,y));
}