编译原理 C-Minus 语义分析(Flex / Bison)

本文介绍了一个C-Minus编译器的语义分析模块实现,详细讲述了如何使用Flex和Bison进行词法、语法及语义分析,重点讨论了错误类型检查和符号表的实现。

C-Minus 源代码 语义分析

一、实现目标

  书接上文,在之前的两篇文章中,我们已经实现了词法分析、语法分析,接下来,我们需要实现的是语义分析。

具体目标:

  1. 在词法及语法分析程序的基础上,编写一个程序对使用类C语言书写的源代码进行语义分析及类型检查,并打印分析结果。
  2. 程序要能够检查源代码中是否符合以下语义要求:
    (1)最低要求3.1:能够实现对整型(int)和浮点型(float)变量的类型检查,两类变量不能相互赋值及运算;仅整型及浮点型变量才能参与算术运算
    (2)其他要求3.2:能判断源代码是否符合以下语义假设并给出相应错误具体位置:函数仅能定义一次、程序中所有变量均不能重名、函数不可嵌套定义
    (3)其他要求3.3:能检查结构体中域是否与变量重名,不同结构体中域是否重名
  3. 报错信息需能定位错误位置

其他要求:
  在本次实验中,我们对C-语言做如下假设,你可以认为这些就是C-语言的特性(注意,假设3、假设4、假设5可能因后面的不同选做要求而有所改变):

  • 假设1:整型(int)变量不能与浮点型(float)变量相互赋值或者相互运算。
  • 假设2:仅有int型变量才能进行逻辑运算或者作为if和 while语句的条件;仅有int型和 float型变量才能参与算术运算。
  • 假设3:任何函数只进行一次定义,无法进行函数声明。
  • 假设4:所有变量(包括函数的形参)的作用域都是全局的,即程序中所有变量均不能重名
  • 假设5:结构体间的类型等价机制采用名等价( Name Equivalence)的方式
  • 假设6:函数无法进行嵌套定义。
  • 假设7:结构体中的域不与变量重名,并且不同结构体中的域互不重名。以上假设1~假设7也可视为要求,违反即会导致各种语 义错误,不过我们只对后面讨论的错误类型进行考查。此外你可以安全地假设输入文件中不包含注释、八进制数、十六进制数以及指数形式的浮点数,也不包含任何词法或语法错误(除了特别说明的针对选做要求的测试)

  你的程序需要对输入文件进行语义分析并检查如下类型的错误,必做部分不会出现结构体和数组:

  • 错误类型1:变量在使用时未经定义。
  • 错误类型2:赋值号两边的表达式类型不匹配。
  • 错误类型3:赋值号左边出现一个只有右值的表达式。
  • 错误类型4:对普通变量使用“(…)”或“()”(函数调用)操作符。
  • 错误类型5:函数在调用时未经定义。
  • 错误类型6:操作数类型不匹配或操作数类型与操作符不匹配(例如整型变量与数组变量相加减,或数组(或结构体)变量与数组(或结构体)结构体变量相加减)。

  除此之外,你的程序可以选择完成以下部分或全部的要求:

  • 选做2.1,这部分是与定义相关的
    • 错误类型7:变量出现重复定义。
    • 错误类型8:函数出现重复定义(即同样的函数名出现了不止一次定义)
  • 选做2.2,这部分是与结构体相关的
    • 类型9:结构体中域与变量重名。
    • 类型10:不同结构体中的域重名。(类型9和10不一定是错误,但需要检查是否重名并进行提示)
    • 错误类型11:结构体的名字与前面定义过的结构体或变量的名字重复。

  以上是根据实验要求给出的错误类型检查示例,你不一定要完全遵循这里给出的错误类型的检查,只要给出符合实验报告语义要求的错误类型检查都可以,需要你在实验报告中说明。这里给出的实验测试代码中的函数均无参数,若完成有参数的函数并进行参数检查有额外加分。此外选做部分均无测试代码,请根据自己给出的错误类型,自行给出测试样例说明。

二、实现过程

内容综述

  • 本文使用线性链表实现符号表
  • 实现了必做部分错误检测
  • 实现了选做2.1定义相关全部类型检测
  • 实现了选做2.2结构体相关全部类型检测
  • 实现了函数参数以及返回值类型的检测

1. 语法分析树实现

// 抽象语法树
typedef struct treeNode
{
   
   
    // 行数
    int line;
    // Token类型
    char *name;
    // 1变量 2函数 3常数 4数组 5结构体
    int tag;
    // 使用孩子数组表示法
    struct treeNode *cld[10];
    int ncld;
    // 语义值
    char *content;
    // 数据类型 int 或 float
    char *type;
    // 变量的值
    float value;
} * Ast, *tnode;

  为了便于获取孩子节点属性,修改了实验二中语法分析树的实现方式,用数组存储子节点。

// 表示当前节点不是终结符号,还有子节点
if (num > 0)
{
   
   
    father->ncld = num;
    // 第一个孩子节点
    temp = va_arg(list, tnode);
    father->cld[0] = temp;
    setChildTag(temp);
    // 父节点行号为第一个孩子节点的行号
    father->line = temp->line;

    if (num == 1)
    {
   
   
        //父节点的语义值等于左孩子的语义值
        father->content = temp->content;
        father->tag = temp->tag;
    }
    else
    {
   
   
        for (i = 1; i < num; i++)
        {
   
   
            temp = va_arg(list, tnode);
            (father->cld)[i] = temp;
            // 该节点为其他节点的子节点
            setChildTag(temp);
        }
    }
}

  为了在语法分析的同时实现语义分析,除了在Bison代码中进行修改外,还需要修改语法分析树节点的构造函数,实现属性/类型/语义值的传递。父节点的类型、语义值可以从子节点处获得。

2. 符号表实现

  我用线性链表实现了符号表,将不同种类的符号组织成不同的表,维护每张表的表头和表尾,从表尾插入,从表头开始遍历。
在这里插入图片描述

2.1 变量符号表
// 变量符号表的结点
typedef struct var_
{
   
   
    char *name;
    char *type;
    // 是否为结构体域
    int inStruc;
    // 所属的结构体编号
    int strucNum;
    struct var_ *next;
}var;
var  *varhead, *vartail;
// 建立变量符号
void newvar(int num,...);
// 变量是否已经定义
int  findvar(tnode val);
// 变量类型
char* typevar(tnode val);
// 这样赋值号左边仅能出现ID、Exp LB Exp RB 以及 Exp DOT ID
int checkleft(tnode val);

  实现了创建遍历符号、变量类型检查、查看变量是否已经定义、检查赋值号左边变量类型的功能函数。为了实现检查结构体域的功能,在符号表节点中设置了inStruc 和 strucNum变量,用于标注该变量属于哪一个结构体。

// 建立变量符号
void newvar(int num, ...)
{
   
   
    va_list valist;
    va_start(valist, num);

    var *res = (var *)malloc(sizeof(var));
    tnode temp = (tnode)malloc(sizeof(tnode));

    if (inStruc && LCnum)
    {
   
   
        // 是结构体的域
        res->inStruc = 1;
        res->strucNum = strucNum;
    }
    else
    {
   
   
        res->inStruc = 0;
        res->strucNum = 0;
    }

    // 变量声明 int i
    temp = va_arg(valist, tnode);
    res->type = temp->content;
    temp = va_arg(valist, tnode);
    res->name = temp->content;

    vartail->next = res;
    vartail = res;
}
// 查找变量
int findvar(tnode val)
{
   
   
    var *temp = (var *)malloc(sizeof(var *));
    temp = varhead->next;
    while (temp != NULL)
    {
   
   
        if (!strcmp(temp->name, val->content))
        {
   
   
            if (inStruc && LCnum) // 当前变量是结构体域
            {
   
   
                if (!temp->inStruc)
                {
   
   
                    // 结构体域与变量重名
                    printf("Error type 9 at Line %d:Struct Field and Variable use the same name.\n", yylineno);
                }
                else if (temp->inStruc && temp->strucNum != strucNum)
                {
   
   
                    // 不同结构体中的域重名
                    printf("Error type 10 at Line %d:Struct Fields use the same name.\n", yylineno);
                }
                else
                {
   
   
                    // 同一结构体中域名重复
                    return 1;
                }
            }
            else // 当前变量是全局变量
            {
   
   
                if (temp->inStruc)
                {
   
   
                    // 变量与结构体域重名
                    printf("Error type 9 at Line %d:Struct Field and Variable use the same name.\n", yylineno);
                }
                else
                {
   
   
                    // 变量与变量重名,即重复定义
                    return 1;
                }
            }
        }
        temp = temp->next;
    }
    return 0;
}

  由于设置了相关属性值,可以实现在寻找变量的同时判断错误类型9和10,这两种错误类型只做提醒,不影响变量的定义以及变量符号表的插入操作。
  符号表的插入和遍历操作大致相同,这里展示变量符号表的相关操作,其他符号表只说明不同的地方。

// 赋值号左边只能出现ID、Exp LB Exp RB 以及 Exp DOT ID
int checkleft(tnode val)
{
   
   
    if (val->ncld == 1 && !strcmp((val->cld)[0]->name, "ID"))
        return 1;
    else if (val->ncld == 4 && !strcmp((val->cld)[0]->name, "Exp") && !strcmp((val->cld)[1]->name, "LB") && !strcmp((val->cld)[2]->name, "Exp") && !strcmp((val->cld)[3]->name, "RB"))
        return 1;
    else if (val->ncld == 3 && !strcmp((val->cld)[0]->name, "Exp") && !strcmp((val->cld)[1]->name, "DOT") && !strcmp((val->cld)[2]->name, "ID"))
        return 1;
    else
        return 0;
}

  在赋值操作中,左值表示地址,这说明表达式“x = 3”是正确的,而“x + 2 = 3”是错误的。这样赋值号左边仅能出现ID、Exp LB Exp RB 以及 Exp DOT ID,因此需要检查赋值号左边的语法分析树节点的子节点是否符合上述三种情况,用来判断是否为对应的错误类型。

2.2 函数符号表实现
// 函数符号表的结点
typedef struct func_
{
   
   
    int tag; //0表示未定义,1表示定义
    char *name;
    char *type;
    // 是否为结构体域
    int inStruc;
    // 所属的结构体编号
    int strucNum;
    char *rtype; //声明返回值类型
    int va_num;  //记录函数形参个数
    char *va_type[10];
    struct func_ *next;
}func;
func *funchead,*functail;
// 记录函数实参
int va_num;
char* va_type[10];
void getdetype(tnode val);//定义的参数
void getretype(tnode val);//实际的参数
void getargs(tnode Args);//获取实参
int checkrtype(tnode ID,tnode Args);//检查形参与实参是否一致
// 建立函数符号
void newfunc(int num, ...);
// 函数是否已经定义
int findfunc(tnode val);
// 函数类型
char *typefunc(tnode val);
// 函数的形参个数
int numfunc(tnode val);
// 函数实际返回值类型
char *rtype[10];
int rnum;
void getrtype(tnode val);
// 创建函数符号
void newfunc(int num, ...)
{
   
   
    int i;
    va_list valist;
    va_start(valist, num);

    tnode temp = (tnode)malloc(sizeof(struct treeNode));

    switch (num)
    {
   
   
    case 1:
        if (inStruc && LCnum)
        {
   
   
            // 是结构体的域
            functail->inStruc = 1;
            functail->strucNum = strucNum;
        }
        else
        {
   
   
            functail->inStruc = 0;
            functail->strucNum = 0;
        }
        //设置函数返回值类型
        temp = va_arg(valist, tnode);
        functail->rtype = temp->content;
        functail->type = temp->type;
        for (i = 0; i < rnum; i++)
        {
   
   
            if (rtype[i] == NULL || strcmp(rtype[i], functail->rtype))
                printf("Error type 12 at Line %d:Func return type error.\n", yylineno);
        }
        functail->tag = 1; //标志为已定义
        func *new = (func *)malloc(sizeof(func));
        functail->next = new; //尾指针指向下一个空结点
        functail = new;
        break;
    case 2:
        //记录函数名
        temp = va_arg(valist, tnode);
        functail->name = temp->content;
        //设置函数声明时的参数
        temp = va_arg(valist, tnode);
        functail->va_num = 0;
        getdetype(temp);
        break;
    default:
        break;
    }
}

  本次实验中,我对函数的声明和定义分别作了处理(进行FunDec和ExtDef规约时作不同处理)。在进行ExtDef规约时可以获取函数的返回值类型,并且同时判断函数的声明返回类型、实际返回值类型是否相同。在FunDec规约时检测函数是否已定义并且设置函数的形参和函数名称。

//定义的参数
void getdetype(tnode val)
{
   
   
    int i;
    if (val != NULL)
    {
   
   
        if (!strcmp(val->name, "ParamDec"))
        {
   
   
            functail->va_type[functail->va_num] = val->cld[0]->content;
            functail->va_num++;
            return;
        }
        for (i = 0; i < val->ncld; ++i)
        {
   
   
            getdetype((val->cld)[i]);
        }
    }
    else
        return;
}
//实际的参数
void getretype(tnode val)
{
   
   
    int i;
    if (val != NULL)
    {
   
   
        if (!strcmp(val->name, "Exp"))
        {
   
   
            va_type[va_num] = val->type;
            va_num++;
            return;
        }
        for (i = 0; i < val->ncld; ++i)
        {
   
   
            getretype((val->cld)[i]);
        }
    }
    else
        return;
}
//检查形参与实参是否一致,没有错误返回0
int checkrtype(tnode ID, tnode Args)
{
   
   
    int i;
    va_num = 0;
    getretype(Args);
    func *temp = (func *)malloc(sizeof(func *));
    temp = funchead->next;
    while (temp != NULL && temp->name != NULL && temp->tag == 1)
    {
   
   
        if (!strcmp(temp->name, ID->content))
            break;
        temp = temp->next;
    }
    if (va_num != temp->va_num)
        return 1;
    for (i = 0; i < temp->va_num; i++)
    {
   
   
        if (temp->va_type[i] == NULL || va_type[i] == NULL || strcmp(temp->va_type[i], va_type[i]) != 0)
            return 1;
    }
    return 0;
}

  由于在Bison代码中,对于函数形参以及实参规约的节点都是父节点,需要用先序遍历获取树结构中所有的参数节点,并进行存储,以便于检测相关类型错误。

2.3 数组符号表实现
// 数组符号表的结点
typedef struct array_
{
   
   
    char *name;
    char *type;
    // 是否为结构体域
    int inStruc;
    // 所属的结构体编号
    int strucNum;
    struct array_ *next;
}array;
array *arrayhead,*arraytail;
// 建立数组符号
void newarray(int num, ...);
// 查找数组是否已经定义
int findarray(tnode val);
// 数组类型
char *typearray(tnode val);

  本次实验中我没有对数组符号做太多处理,该符号表和变量符号表操作基本一致。

2.4 结构体符号表实现
// 结构体符号表的结点
typedef struct struc_
{
   
   
    char *name;
    char *type;
    // 是否为结构体域
    int inStruc;
    // 所属的结构体编号
    int strucNum;
    struct struc_ *next;
}struc;
struc *struchead, *structail;
// 建立结构体符号
void newstruc(int num, ...);
// 查找结构体是否已经定义
int findstruc(tnode val);
// 当前是结构体域
int inStruc;
// 判断结构体域,{ 和 }是否抵消
int LCnum;
// 当前是第几个结构体
int strucNum;

  由于错误类型11需要检测结构体定义是否和之前定义的结构体、变量名称一致,因此在检测重复定义时做一些改动:

// 结构体是否和结构体或变量的名字重复
int findstruc(tnode val)
{
   
   
    struc *temp = (struc *)malloc(sizeof(struc *));
    temp = struchead->next;
    while (temp != NULL)
    {
   
   
        if (!strcmp(temp->name, val->content))
            return 1;
        temp = temp->next;
    }
    if (findvar(val) == 1)
        return 1;
    return 0;
}

3. Flex/Bison代码分析

  需要实现在语法分析的同时进行语义分析,因此在规约时对某些类型的节点需要进行符号表插入、遍历以及错误类型的检查工作。

错误类型定义:

  • 错误类型1:变量在使用时未经定义。
  • 错误类型2:赋值号两边的表达式类型不匹配。
  • 错误类型3:赋值号左边出现一个只有右值的表达式。
  • 错误类型4:对普通变量使用“(…)”或“()”(函数调用)操作符。
  • 错误类型5:函数在调用时未经定义。
  • 错误类型6:操作数类型不匹配或操作数类型与操作符不匹配(例如整型变量与数组变量相加减,或数组(或结构体)变量与数 组(或结构体)结构体变量相加减)。
  • 错误类型7:变量出现重复定义。
  • 错误类型8:函数出现重复定义(即同样的函数名出现了不止一次定义)
  • 错误类型9:结构体中域与变量重名
  • 错误类型10:不同结构体中的域重名(类型9和10不一定是错误,但需要检查是否重名并进行提示)
  • 错误类型11:结构体的名字与前面定义过的结构体或变量的名字重复。
  • 错误类型12:函数声明的返回类型和实际返回值类型不一致
  • 错误类型13:函数定义的形参与调用的实参数量或类型不一致
3.1 错误类型1
Exp:ID {
   
   
		$$=newAst("Exp",1,$1); 
		// 错误类型1:变量在使用时未经定义
		if(!findvar($1)&&!findarray($1))
			printf("Error type 1 at Line %d:undefined variable %s\n",yylineno,$1->content);
		else 
			$$->type=typevar($1);
	}

  操作数ID会被规约为Exp,ID就是变量的名称,因此需要在这里检测变量是否已经定义。

3.2 错误类型2、3
Exp:Exp ASSIGNOP Exp{
   
   
		$$=newAst("Exp",3,$1,$2,$3); 
		// 当有一边变量是未定义时,不进行处理
		if($1->type==NULL || $3->type==NULL){
   
   
			return;
		}
		// 错误类型2:赋值号两边的表达式类型不匹配
		if(strcmp($1->type,$3->type))
			printf("Error type 2 at Line %d:Type mismatched for assignment.\n ",yylineno);
		// 错误类型3:赋值号左边出现一个只有右值的表达式
		if(!checkleft($1))
			printf("Error type 3 at Line %d:The left-hand side of an assignment must be a variable.\n ",yylineno);
	}

  由于在进行语法分析树建立时修改了节点构造函数,使得子节点类型能够传递给父节点,在检测赋值号两端表达式类型时就可以直接检测表达式节点的type属性是否相同。这里判断了type属性是否为NULL(表达式是否未定义),如果不进行相关处理,碰到未定义的表达式,其type属性为NULL,直接对NULL使用strcmp函数会报出段错误。错误类型3和错误类型2都是在规约到ASSIGNOP符号时进行检测。

3.3 错误类型4、5、13
Exp:ID LP Args RP {
   
   
		$$=newAst("Exp",4,$1,$2,$3,$4); 
		// 错误类型4:对普通变量使用“(...)”或“()”(函数调用)操作符
		if(!findfunc($1) && (findvar($1)||findarray($1)))
			printf("Error type 4 at Line %d:'%s' is not a function.\n ",yylineno,$1->content);
		// 错误类型5:函数在调用时未经定义
		else if(!findfunc($1))
			printf("Error type 5 at Line %d:Undefined function %s\n ",yylineno,$1->content);
		// 函数实参和形参类型不一致
		else if(checkrtype($1,$3)){
   
   
			printf("Error type 13 at Line %d:Function parameter type error.\n ",yylineno
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值