使用c/c++实现语义分析器
一、前言
这次编译原理实验要求实现语义分析,但最近空闲时间比较少,只能快速过一遍基础知识硬着头皮写了。但由于之前我设计文法大多只是为了测试使用,所以文法很不规范,这直接导致了语义子程序十分难实现,所以要想实现一个好的语义分析器,必须先设计好你的文法。
需要注意的是,这个语义分析器是建立在我之前写的语法分析器之上的。
二、具体实现
由于之前语法分析实现的是SLR(1)语法分析器,是一种自底向上的分析方法,所以这里我们使用语法制导翻译,即在进行语法分析的过程中(在归约的时候)实现语义分析。
为了实现语义分析,我们需要将归约的字符串与一系列的动作关联起来,也就是,将有语义分析意义的产生式和语义子程序关联起来,每当使用这个产生式进行归约的时候,调用对应的语义子程序对符号的属性(这里我只用到了综合属性,即该属性的值只通过该语法树中该符号对应节点的孩子节点的属性)进行计算。
先给出文法,需要注意的是,这个文法是有问题的!只能简单用用,不要深究。
1、需要修改或添加的结构体
intermediate_code.h
这个头文件声明了中间代码的结构体,lineNum是中间代码的编号,从100开始;code是生成的三地址码。
如100: a = b + 4,100对应的lineNum,a = b + 4对应的code。
symbol_line.h
这个是符号表单元,这里我简化了结构,仅仅用作变量查重(重复声明)和类型冲突(如boolean和int不能做运算)。
addr表示变量的名称,value表示变量的值,type表示变量的类型。比如一个int类型的变量a的值是4,则addr是a,value是4,type是int。
需要注意的是,我这里也将临时变量存入符号表了,而且value的值是有bug的,不过没时间改了,需要的话可以自己改改。
word.h
因为语法分析的时候需要一些属性,所以在词法分析后生成的符号串中,要保存一些属性。这里相比之前添加了addr、extra和variable。code没用,key存的是关键字,比如int、while之类的,但要注意的是,对于变量,key存的是id,对于数字,key存的是num。
当字符是数字的时候,addr存的是该数字的值,如12;当字符是变量的时候,addr存的是变量名,如a。
当字符是数值类型的时候(int、double和boolean),extra存的就是对应类型;当字符是运算符的时候(+、-、*、/),extra存的就是对应的运算符。
variable没有用到,可以忽略。
2、重要结构介绍
int tempNum = 0; // 临时变量的序号
int lineNum = 100; // 中间代码序号
// 中间代码
IntermediateCodeLine lines[1000]; // 中间代码行数组
int lineTop = 0; // 数组尾
// 符号表
SymbolLine symbolTable[1000]; // 符号表
int tableTop = 0; // 符号表尾
// 循环栈
// 栈顶记录的是最内层的循环开始时的中间代码在数组中的下标
int loopStack[100]; // 循环栈
int loopStackTop = 0; // 循环对应的中间代码下标
这里中间代码数组和符号表没什么好说的,需要注意的是循环栈,栈顶记录的是当前最内层循环开始对应的中间代码在数组中的下标,我们遇到while的时候需要先把此时到达的中间代码行空下,等到将while的整个结构归约的时候再填上。因为刚归约到while的时候,我们是不知道while循环结束的条件(还没归约到)和要跳转的中间代码行序号(因为while循环体中的代码还没归约到)。
举个例子,下面是我定义的循环的产生式:
W就是while归约成的,C是条件,S是循环体中的代码。归约到while的时候我们就需要准备中间代码中的跳转语句了(goto),但此时后面的C和S还没有归约到,所以我们不知道循环结束的条件是什么,也不知道S中会产生几条中间代码,也就意味着我们不知道要跳转的中间代码的序号是多少。所以在归约到while的时候,需要空出一行来写跳转的中间代码,等到S->W( C ){ S }归约的时候补上跳转的语句。
3、重要函数介绍
实际上没什么重要函数,只需要写几个语义子程序就行。
先看看语义分析的头文件。
#ifndef __SEMANTICALANALYSIS_H__
#define __SEMANTICALANALYSIS_H__
#include "item.h"
#include "production_rule.h"
#include "state.h"
#include "analysis_table_cell.h"
#include "word.h"
#include "symbol.h"
// 获取一个临时变量
string getTempV();
// 产生中间代码
WORD createCode(int productionId, string leftPart, WORD* wordsInStack, int top);
// 打印中间代码
void printIntermediateCodeLine();
// 打印符号表(现在符号表有问题,所以暂时不用)
void printSymbolTable();
#endif
getTempV()
这个就是获取一个临时变量的名称。因为在三地址码中,一个表达式能表达出的变量数目有限。如要求d = a + b + c的话,需要改成:
t0 = b + c
d = a + t0
这里t0就是临时变量,在运算比较复杂的情况下,会需要很多临时变量,所以需要一个函数来专门获取。这个函数很好理解。
// 获取一个临时变量
string getTempV(){
return "sys_temp" + to_string(tempNum++);
}
createCode()
这个函数就是语法分析归约的时候调用的生成中间代码的函数,也很好理解,就是根据归约用的产生式来执行对应的语义子程序。同时是语义分析的入口。
// 语义分析,产生中间代码
WORD createCode(int productionId, string leftPart, WORD* wordsInStack, int top){
// 封装返回的字符
WORD result;
result.key = leftPart;
result.addr = wordsInStack[top - 1].addr;
result.extra = wordsInStack[top - 1].extra;
// 执行语义子程序
if(productionId == 18){
SemanticSubroutine_18(wordsInStack[top - 1]);
}
if(productionId == 13){
result = SemanticSubroutine_13(wordsInStack[top - 3], wordsInStack[top - 2], wordsInStack[top - 1], leftPart);
}
if(productionId == 19){
SemanticSubroutine_19(wordsInStack[top - 4], wordsInStack[top - 2], wordsInStack[top - 5]);
}
if(productionId == 6){
SemanticSubroutine_6();
}
if(productionId == 1){
SemanticSubroutine_1(wordsInStack[top - 5]);
}
if(productionId == 9){
result = SemanticSubroutine_9(wordsInStack[top - 3], wordsInStack[top - 2], wordsInStack[top - 1], leftPart);
}
return result;
}
checkType()
这个函数是用来检查两个变量是否能进行运算,比如boolean和int不能进行加减乘除。
这个用在9. C -> EUE和13. E -> EOE中,即用在9号和13号产生式的语义子程序中,判断两个变量是否能进行比较或者算术运算。并返回兼容的不同类型中更高级的那个(返回后存在对应临时变量的符号表单元中)。
// 检查类型兼容性,并返回更高级的类型
string checkType(string addr1, string addr2){
// 默认是int类型
string type1 = "int";
string type2 = "int";
// 根据变量名addr1和addr2从符号表查对应变量的类型
// 如果不在符号表中,那就是数字了,默认是上面初始化的int
int id1 = getSymbolIdx(addr1);
if(id1 != -1){
type1 = symbolTable[id1].type;
}
int id2 = getSymbolIdx(addr2);
if(id2 != -1){
type2 = symbolTable[id2].type;
}
// 如果有一个是bool,就无法运算(因为我这里只定义了int、double和boolean三种类型)
if(type1.compare("boolean") == 0 || type2.compare("boolean") == 0){
cout << addr1 << "(" << type1 << ") cant compute with " << addr2 << "(" << type2 << ")..." << endl;
exit(0);
}
// 一般double和int进行计算,都会转换成double类型
if(type1.compare("double") == 0 || type2.compare("double") == 0){
return "double";
} else {
return "int";
}
}
SemanticSubroutine_13()
第13号产生式 E -> EOE对应的语义子程序。就是加段中间代码(x = x + x)。
// 产生式13对应的语义子程序
WORD SemanticSubroutine_13(WORD v1, WORD op, WORD v2, string leftPart){
// 类型检查
string type = checkType(v1.addr, v2.addr);
WORD result;
// 获取临时变量
result.addr = getTempV();
result.key = leftPart;
// 进行运算检测
// 参与运算的额数字
int v1_d = -1;
int v2_d = -1;
// 判断v1是否为符号表中的变量,是的话从符号表中取值
int id = getSymbolIdx(v1.addr);
if(id == -1){
v1_d = atoi(v1.addr.c_str());
} else {
v1_d = atoi(symbolTable[id].value.c_str());
}
// 判断v2是否为符号表中的变量,是的话从符号表中取值
id = getSymbolIdx(v2.addr);
if(id == -1){
v2_d = atoi(v2.addr.c_str());
} else {
v2_d = atoi(symbolTable[id].value.c_str());
}
// 进行符号运算(在extra中保存着运算符号)
if(op.extra.compare("+") == 0){
lines[lineTop].code = result.addr + " = " + v1.addr + " + " + v2.addr;
lines[lineTop].lineNum = lineNum;
lineTop++;
lineNum++;
result.variable = true;
// 临时变量加入符号表
symbolTable[tableTop].addr = result.addr;
symbolTable[tableTop].type = type;
symbolTable[tableTop].value = v1_d + v2_d;
tableTop++;
}
if(op.extra.compare("-") == 0){
lines[lineTop].code = result.addr + " = " + v1.addr + " - " + v2.addr;
lines[lineTop].lineNum = lineNum;
lineTop++;
lineNum++;
result.variable = true;
// 临时变量加入符号表
symbolTable[tableTop].addr = result.addr;
symbolTable[tableTop].type = type;
symbolTable[tableTop].value = v1_d - v2_d;
tableTop++;
}
if(op.extra.compare("*") == 0){
lines[lineTop].code = result.addr + " = " + v1.addr + " * " + v2.addr;
lines[lineTop].lineNum = lineNum;
lineTop++;
lineNum++;
result.variable = true;
// 临时变量加入符号表
symbolTable[tableTop].addr = result.addr;
symbolTable[tableTop].type = type;
symbolTable[tableTop].value = v1_d * v2_d;
tableTop++;
}
if(op.extra.compare("/") == 0){
lines[lineTop].code = result.addr + " = " + v1.addr + " / " + v2.addr;
lines[lineTop].lineNum = lineNum;
lineTop++;
lineNum++;
result.variable = true;
// 临时变量加入符号表
symbolTable[tableTop].addr = result.addr;
symbolTable[tableTop].type = type;
symbolTable[tableTop].value = v1_d / v2_d;
tableTop++;
}
return result;
}
SemanticSubroutine_19()
19号产生式,即 E -> TE=E; 对应的语义子程序。主要是生成一条中间代码(x = x)和添加变量至符号表,可以判断是否有变量重复声明。
// 产生式19对应的语义子程序
void SemanticSubroutine_19(WORD v1, WORD v2, WORD type){
//cout << v2.key << "--" << v1.key << endl;
// 判断变量是否重复
if(-1 != getSymbolIdx(v1.addr)){
cout << "multi declare of variable [" << v1.addr << "]..." << endl;
exit(0);
}
// 将变量添加到符号表
symbolTable[tableTop].addr = v1.addr;
// 变量类型(int、double、boolean)保存在extra中
symbolTable[tableTop].type = type.extra;
// 变量赋值
string value;
int id = getSymbolIdx(v2.addr);
if(id == -1){
value = v2.addr;
} else {
value = symbolTable[id].value;
}
symbolTable[tableTop].value = value;
tableTop++;
// 中间代码
lines[lineTop].code = v1.addr + " = " + v2.addr;
lines[lineTop].lineNum = lineNum;
lineTop++;
lineNum++;
}
SemanticSubroutine_6()
6号产生式,即W -> while对应的语义子程序,就是上面说过的,空出一行来,等着循环体整体归约的时候补上。
// 产生式6对应的语义子程序
void SemanticSubroutine_6(){
// 空出一行来等着循环体整体归约的时候补上
lines[lineTop].code = "blank";
lines[lineTop].lineNum = lineNum;
// 记录最内层循环开始的行数
loopStack[loopStackTop] = lineTop;
loopStackTop++;
lineNum++;
lineTop++;
}
SemanticSubroutine_1()
1号产生式,即 S -> W©{S}对应的语义子程序,就是上面说过的,补上之前空出的一行,和加一行往回跳转的语句。
// 产生式1对应的语义子程序
void SemanticSubroutine_1(WORD t){
// 找到最内层循环开始的行数
int startLine = loopStack[--loopStackTop];
// 补上当初空出来的那行条件跳转语句
lines[startLine].code = "if not " + t.extra + " then goto " + to_string(lineNum + 1);
//cout << lines[startLine].code << endl;
// 产生跳回到while判断条件的中间代码
lines[lineTop].code = "goto " + to_string(lines[startLine].lineNum);
lines[lineTop].lineNum = lineNum;
lineTop++;
lineNum++;
}
SemanticSubroutine_9()
9号产生式,即C -> EUE对应的语义子程序,主要是判断两个变量是否能比较(是否会产生类型冲突)和记录比较的条件,在补上当初空出来的那行条件跳转语句的时候要用到。
// 产生式9对应的语义子程序
WORD SemanticSubroutine_9(WORD v1, WORD op, WORD v2, string leftPart){
// 类型检查
checkType(v1.addr, v2.addr);
WORD result;
result.key = leftPart;
// 记录判断条件,补上当初空出来的那行条件跳转语句的时候要用到
result.extra = v1.addr + " " + op.extra + " " + v2.addr;
return result;
}
三、用法
在main.c文件中修改输入的代码就可以了,因为语义子程序是写死的,所以产生式不能动。
四、测试
正常:
重复声明:
运算时类型冲突:
五、后记
实际上这次实现的代码质量非常差,没有足够的时间构建代码的结构,也没有事先写出一个合适的文法来。所以这篇博客比较适合了解思想,而非学习代码内容。希望大家能有所收获,要是有什么问题可以评论一下,谢谢大家的观看!