动手实现编译器(十一)——函数功能(第二部分)

上一节中,我们实现了函数的定义,在这一节中,我们会实现调用函数并返回一个值。具体来说:

  • 定义一个函数
  • 调用一个目前无法使用的单一值的函数
  • 从函数返回一个值
  • 将函数调用用作语句和表达式
  • 确保void函数永远不会返回值和非void函数必须返回一个值

函数调用的SysY语法定义:

语句: Stmt → Ident ‘(’ [FuncRParams] ‘)’ ‘;’ | ‘return’ [Exp] ‘;’

该函数有一个名称,后跟一对括号,在括号中必须只有一个参数。它既可以作为表达式又可以作为独立语句。

修改词法分析

我们现在的词法分析存在这样一个问题:

   x= a + b;
   x= a(5) + b;

我们不能正确分辨出a是变量还是函数。
首先,我们增加了标识符的属性,结构类型,表示这个标识符是一个函数还是变量。

// 结构类型
enum
{
    S_VARIABLE, S_FUNCTION
};

// 数据类型
enum
{
    P_VOID, P_INT
};

// 符号表结构体
struct symbol_table
{
    char *name;			        // 符号名
    int stype;			        // 结构类型
    int type;                   // 类型void或int
    int endlabel;			    // 函数的结束标签
};

同时修改增加全局变量函数为

// 将全局变量添加到符号表,并返回符号表中的位置
int add_global(char *name, int stype, int endlabel)
{
    int y;
    // 如果已经在符号表中,则返回现有位置
    if ((y = find_global(name)) != -1)
        return y;
    // 获得一个新的位置,并填入信息和返回位置
    y = new_global();
    Tsym[y].name = strdup(name);
    Tsym[y].stype = stype;
    Tsym[y].endlabel = endlabel;
    return y;
}

调用add_global()var_declaration()函数也要修改

// 分析变量声明
void var_declaration()
{
    // 检查当前单词是否为“int”,后跟一个标识符和一个分号
    match(T_KEYINT, "int");
    ident();
    add_global(Text, S_VARIABLE, 0);
    Tsym[t].type = P_INT;
    arm_global_sym(Text);
    semi();
}

接着回来考虑我们刚才的那个问题,如何区分变量和函数。我的方法是向前看一个符号,看看是否有“(”。如果有,则是函数调用。但是这样做的话,我们会失去当前的单词。为了解决这个问题,我修改了scan()函数,这样我们就可以放回不需要的单词:当我们获得下一个单词是丢弃单词的时候,它将返回。

// 被丢弃单词的指针
struct token *Rejtoken = NULL;

// 丢弃刚刚扫描的单词
void reject_token(struct token *t)
{
    if (Rejtoken != NULL)
    {
        fprintf(stderr, "Can't reject token twice on line %d\n", Line);
        exit(1);
    }
    Rejtoken = t;
}

// 扫描并返回在输入中找到的下一个单词。
// 如果标记有效则返回 1,如果没有标记则返回 0
int scan(struct token *t)
{
    int c, tokentype;
    // 如果有被丢弃的单词,将其返回
    if (Rejtoken != NULL)
    {
        t = Rejtoken;
        Rejtoken = NULL;
        return 1;
    }
	/*继续正常扫描... ...*/
}

接下来,我们考虑另一个问题,如何分析return语句。首先,添加“T_T_RETURN”单词类型。
然后加入对return的分析在match_keyword()函数中。

        case 'r':   if(!strcmp(s, "return")) return (T_RETURN);
                    break;

修改语法分析

分析函数调用

// 分析有单个参数的函数调用并返回其AST树
struct ASTnode *functioncall()
{
    struct ASTnode *tree;
    int id;
    // 检查标识符是否已定义,然后为其创建一个叶节点
    if ((id = find_global(Text)) == -1)
    {
        fprintf(stderr, "Undeclared function on line %d\n", Line);
        exit(1);
    }
    // 匹配'('
    lparen();
    // 分析接下来的表达式
    tree = binexpr(0);
    // 构建函数调用AST节点,将函数的返回类型存储为此节点的类型,
    // 同时记录函数的符号ID
    tree = mkastunary(A_FUNCTIONCALL, tree, id);
    // 匹配')'
    rparen();
    return tree;
}

将函数作为表达式调用

我们在primary()中区分变量名和函数调用,并把函数作为表达式调用。

// 解析一个整数单词并返回表示它的AST节点
struct ASTnode *primary()
{
    struct ASTnode *n;
    int id;
    switch (Token.token)
    {
        // 对于整数单词,为其生成一个AST叶子节点
        case T_INT:  
            n = mkastleaf(A_INT, Token.intvalue);
            break;
        // 对于标识符,检查存在并为其生成一个AST叶子节点
        case T_IDENT:
            // 扫描下一个字符判断这单词是变量还是函数调用
            scan(&Token);
            // 如果是'(',那这是函数调用
            if (Token.token == T_LPAREN) return functioncall();
            // 如果不是函数调用,则丢弃新单词
            reject_token(&Token);
            // 检查单词是否存在
            id = find_global(Text);
            if (id == -1)
            {
                fprintf(stderr, "Unknown variable %s on line %d\n", Text, Line);
                exit(1);
            }
            n = mkastleaf(A_IDENT, id);
            break;
        default:
            fprintf(stderr, "syntax error, token %s on line %d\n", Token.token, Line);
            exit(1);
    }
    // 扫描下一个单词,并返回左节点
    scan(&Token);
    return n;
}

将函数作为语句调用

当我们尝试将函数作为语句调用时,我们遇到了本质上与把函数作为表达式调用相同的问题。
考虑下面的代码:

  x = 20;
  x(21);

我们要区分变量赋值语句和函数调用语句,这与区分变量和函数的方法类似:

// 分析赋值语句
struct ASTnode * assignment_statement()
{
    struct ASTnode *left, *right, *tree;
    int id;
    // 检查标识符
    ident();
    // 如果下一个单词是'(',则这是函数调用
    if (Token.token == T_LPAREN) return functioncall();
    /*不是函数调用,进行赋值操作... ...*/
}

分析RETURN语句

分析很简单:‘return’, ‘(’, call binexpr(), ‘)’。困难的是类型的检查。
首先,当我们到达 return 语句时,我们需要知道我们实际上在哪个函数中。为此,我们定义一个全局变量表示当前所在的函数。

int             Functionid;		        // 当前函数的符号id

修改函数定义的函数,使每次输入函数声明时都设置了Functionid,我们可以重新解析和检查return语句的语义。

// 分析简单函数声明
struct ASTnode *function_declaration()
{
    struct ASTnode *tree, *finalstmt;
    int nameslot, endlabel;
    // 匹配'void'或'int'、函数名和'(' ')',
    // 但不做任何处理
    if (Token.token == T_VOID || Token.token == T_KEYINT)
    {
        scan(&Token);
    }
    else
    {
        fprintf(stderr, "Void or int expected on line %d\n", Line);
        exit(1);
    }
    ident();
    // 获取结束标签的label-id,
    // 将函数添加到符号表中,
    // 将Functionid设置为函数的符号id
    endlabel = label();
    nameslot = add_global(Text, S_FUNCTION, endlabel);
    Functionid = nameslot;
    lparen();
    rparen();
    // 获得代码块的AST树
    tree = Block_statement();
    // 如果函数类型不是VOID,
    // 检查语句块中的最后一个AST操作是否为return语句
    if (Token.token != T_VOID)
    {
    	Tsym[nameslot].type = P_INT;
        finalstmt = (tree->op == A_GLUE) ? tree->right : tree;
        if (finalstmt == NULL || finalstmt->op != A_RETURN)
        {
            fprintf(stderr, "No return for function with non-void type on line %d\n", Line);
            exit(1);
        }
    }
	else
    {
        Tsym[nameslot].type = P_VOID;
    }
    // 返回具有函数符号位置和语句块子树的A_FUNCTION节点
    return mkastunary(A_FUNCTION, tree, nameslot);
}

这里用了A_RETURN一个新的AST操作类型,它返回子节点中的表达式树。

// 分析return语句,并返回其AST树
static struct ASTnode *return_statement()
{
    struct ASTnode *tree;
    int functype;
    // 如果是void函数,不返回值,报错
    if (Tsym[Functionid].type == P_VOID)
    {
        fprintf(stderr, "Can't return from a void function on line %d\n", Line);
        exit(1);
    }
    // 匹配'return'
    match(T_RETURN, "return");
    // 分析接下来的语句
    tree = binexpr(0);
    // 添加一个A_RETURN节点
    tree = mkastunary(A_RETURN, tree, 0);
    // 匹配";"
    semi();
    return tree;
}

修改代码生成器

code_generator()中增加对return和函数调用的分析:

        case A_RETURN: arm_return(leftreg, Functionid); return NOREG;
        case A_FUNCTIONCALL: return arm_call(leftreg, n->v.id);

A_RETURN不返回值,因为它不是表达式。A_FUNCTIONCALL当然是表达式,需要返回值。

修改汇编

新增汇编代码如下:

// 使用给定寄存器中的一个参数调用函数,返回带有结果的寄存器
int arm_call(int r, int id)
{
    int outr = arm_alloc_register();
    fprintf(Outfile, "\tmov\tr0, r%d\n", r);
    fprintf(Outfile, "\tbl\t%s\n", Tsym[id].name);
    fprintf(Outfile, "\tmov\tr%d, r0\n", outr);
    arm_free_register(r);
    return outr;
}

// 生成return语句代码
void arm_return(int reg, int id)
{
    if(Tsym[id].type == P_INT)
    {
        fprintf(Outfile, "\tmov\tr0, r%d\n", reg);
    }
    else
    {
        fprintf(stderr, "Bad function type in cgreturn:%d on line %d\n", Tsym[id].type, Line);
        exit(1);
    }
    if(strcmp(Tsym[id].name,"main"))
    {
        arm_function_postamble();
        fprintf(Outfile, "\tbx  lr\n");
    }
}

代码重构

正如前文所讲的,我们是一边想一边做的,前面的结构总会不符合后面的需求,所以会面临代码重构的问题。这次重构的目的是为了让代码更加贴合语言定义。

语句块: Block → ‘{’ { BlockItem } ‘}’
语句块项: BlockItem → Decl | Stmt
语句: Stmt → LVal ‘=’ Exp ‘;’ | [Exp] ‘;’ | Block
| ‘if’ '( Cond ‘)’ Stmt [ ‘else’ Stmt ]
| ‘while’ ‘(’ Cond ‘)’ Stmt
| ‘return’ [Exp] ‘;’

我们可以看到语法中定义一个语句块,由语句块调用定义变量和单个语句,然后单个语句实现IF、WHILE、RETURN等语句。所以,我们也把代码结构改成类似的,但我们把定义变量放在单个语句中实现。
先定义一个分析单个语句的函数

// 分析一条语句,并返回其AST树
struct ASTnode *single_statement()
{
    switch (Token.token)
    {
        case T_PRINT:   return print_statement();
        case T_KEYINT:  var_declaration();  return NULL; // 没有AST树生成
        case T_IDENT:   return assignment_statement();
        case T_IF:      return if_statement();
        case T_WHILE:   return while_statement();
        case T_RETURN:  return return_statement();
        default:    fprintf(stderr, "Syntax error, token:%d on line %d\n", Token.token, Line);
                    exit(1);
    }
}

然后修改Block_statement()函数,使其调用single_statement()函数来分析

// 分析语句块并返回其AST
struct ASTnode *Block_statement()
{
    struct ASTnode *left = NULL;
    struct ASTnode *tree;
    // 匹配"{"
    lbrace();
    while (1)
    {
        // 分析单个语句
        tree = single_statement();
        // 对于每个新树,如果左子树为空,则将其保存在左子树中,
        // 否则将左子树和新树合并
        if (tree)
        {
            if (left == NULL)   left = tree;
            else    left = mkastnode(A_GLUE, left, NULL, tree, 0);
        }
        // 遇到"}"时,跳过并返回AST
        if (Token.token == T_RBRACE)
        {
            rbrace();
            return left;
        }
    }
}

测试结果

输入:

int ant()
{
	return 20;
}

int main()
{
	int i;
	i=1;
	print 10;
	i = ant(15);
	print i;
	print ant(15) + 10;
	return 0;
}

输出(out.s):

	.text
	.global __aeabi_idiv
	.section	.rodata
	.align  2
.LC0:
	.ascii  "%d\012\000"
	.text
	.align  2
	.globl	ant
	.type	ant, %function
ant:
	push    {fp, lr}
	add     fp, sp, #4
	mov	r4, #20
	mov	r0, r4
	pop     {fp, pc}
	bx  lr
	pop     {fp, pc}
	.text
	.comm	i,4,4
	.text
	.align  2
	.globl	main
	.type	main, %function
main:
	push    {fp, lr}
	add     fp, sp, #4
	mov	r5, #1
	ldr	r3, .L2+8
	str	r5, [r3]
	mov	r4, #10
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	mov	r4, #15
	mov	r0, r4
	bl	ant
	mov	r5, r0
	ldr	r3, .L2+8
	str	r5, [r3]
	ldr	r3, .L2+8
	ldr	r4, [r3]
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	mov	r4, #15
	mov	r0, r4
	bl	ant
	mov	r5, r0
	mov	r4, #10
	add	r5, r5, r4
	mov     r1, r5
	ldr     r0, .L3
	bl      printf
	mov	r4, #0
	mov	r0, r4
	pop     {fp, pc}
.L3:
	.word   .LC0
.L2:
	.word ant
	.word main
	.word i

输出(out):

10
20
30

总结

在这一节中,我们实现了一个简单版本的函数调用,函数返回。这不是微不足道的,但我认为变化大多是明智的。我们可以看到,在这里我们的全局变量变成了局部变量,但是实现的方式仍是全局变量。在下一节中,我们将正确实现全局变量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
这个包主要实现了最简单的Lex和Yacc环境,包含了最少的但是必须的文件。<br>同时还包含了一个MinGW的GNU的C++编译器环境:)可以直接使用:)<br><br>这个包包含的文件列表:<br><br>MinGW GNU的C/C++编译程序(windows版本)<br>bison.exe GNU的yacc程序<br>bison.hairy GNU的yacc程序运行需要的文件<br>bison.simple GNU的yacc程序运行需要的文件<br>flex.exe GNU的lex程序<br>ini.bat 这个lex和yacc环境的环境变量配置<br>lexyacc.bat 这个lex和yacc环境的启动程序<br>Readme.txt 本说明文件<br><br>使用方法:<br><br>1. 鼠标双击lexyacc.bat文件运行<br>2. 在弹出的DOS命令行中利用CD切换到你的lex和yacc源文件所在的目录(calc)<br>3. 对lex源文件执行flex calc.l<br>4. 对yacc源文件执行bison -d calc.y<br><br>经过上面的四个步骤就生成了你需要的C/C++源文件,剩下的事情就是编译<br>这里生成的C/C++源程序了。<br><br>5. g++ lex.yy.c calc.tab.c -o calc<br><br>最后谢谢您的使用,也希望您提出宝贵的意见或者建议,我会认真考虑您的<br>意见或者建议的。可以发邮件到[email protected]和我联系。<br><br>博客:http://blog.csdn.net/pandaxcl<br>论坛:http://www.autodev.net<br><br><br>大家好,本人历时两年构思了四年,目前完成了一半的自动化C++程序设计代码库<br>(autocxx)总算可以拿出来见人了,大家多到论坛(http: //www.autodev.net)<br>或者博客(http://blog.csdn.net/pandaxcl)提些意见吧:)代码是开源的;)<br>下面是整个项目的文档目录:<br><br> * 自动化C++程序设计<br> * 基础篇<br> * C++里的模板语言<br> * 静态诊断<br> * 为什么说模版是C++的子语言<br> * 静态数学运算<br> * 静态选择结构<br> * 静态循环结构<br> * 运算结果的保存<br> * 基本类型<br> * nil类型<br> * t类型<br> * any类型<br> * text类型<br> * kind类型<br> * 类型串类型p<br> * 将C++的模版语言规范化<br> * is_same元函数<br> * 选择结构select<br> * partition元函数<br> * 类型串生成元函数mkps<br> * length元函数<br> * capacity元函数<br> * at元函数<br> * range元函数<br> * 静态循环(loop)<br> * 针对PS的静态循环(loop_ps)<br> * 自动生成函数调用(eloop)<br> * 针对PS自动生成函数调用(eloop_ps)<br> * p的其它一些重要的辅助函数<br> * join元函数<br> * cut元函数<br> * count元函数<br> * index元函数<br> * exist元函数<br> * repeat元函数<br> * resize元函数<br> * reverse元函数<br> * replace元函数<br> * unique元函数<br> * filter元函数<br> * map元函数<br> * reduce元函数<br> * 自动生成C++类<br> * scatter类<br> * tuple结构<br> * 分析C++类层次<br> * 分析C++类结构<br> * 类是否拥有指定参数的成员函数<br> * 类是否存在指定名称的成员函数<br> * 类是否存在指定名称的成员变量<br> * 类是否存在指定名称的子类(型)<br> * 分析C++重载函数<br> * 分析函数参数的数量<br> * 分析函数参数的种类<br> * 存在的问题<br> * C++自动化<br> * 应用篇<br> * C++静态计算器<br> * 顺序计算器<br> * 带括号顺序计算器<br> * 加减乘除带括号计算器<br> * 前缀表达式静态计算器<br> * lambda演算<br> * 基本LISP解释器<br> * 基本静态LISP解释器<br> * 扩展静态LISP解释器<br> * 静态语言解释器的生成器<br> * 问题陈述<br> * 词法分析器<br> * 语法规则表述<br> * 语法规则响应器<br> * 语法分析实现<br> * 使用前面的LEX和YACC实现静态数据库(SQL)语言<br> * 观察者模式的C++自动化实现<br> * 什么是观察者模式<br> * 观察者模式普通实现<br> * 观察者模式中的必备元素<br> * 将观察者模式普通实现自动化<br> * 将观察者模式标准化<br> * 一个使用自动化观察者模式的复杂例子<br> * 撤销和重做(Undo/Redo)的C++自动化实现<br> * 交换函数swap功能的讨论<br> * 撤销和重做原理<br> * 撤销和重做的基本架构<br> * 将撤销和重做的基本架构模组化<br> * 处理多类型的对象以及命令管理<br> * 扩展的框架代码<br> * 处理复合对象<br> * 自动化的用户界面(UI)<br> * 高级篇<br> * 应用BOOST的MPL库<br> * 采用OO思想进行框架设计<br> * 文本编辑器<br> * 图形用户界面<br> * 游戏引擎架构
编译器的选择: 我选择的是TINY编译器,TINY是一种简单的编程语言,它的语法规则比较简单,适合初学者学习编译原理。TINY编译器的源代码可以在《编译原理及实践》一书中找到。 语义分析源程序的理解: TINY编译器的语义分析主要是在语法分析的基础上进行的。在语法分析阶段,我们已经确定了程序的语法正确性,而在语义分析阶段,我们需要进一步确定程序的语义正确性。TINY编译器的语义分析主要包括以下几个方面: 1.类型检查:TINY编译器支持整型和实型两种数据类型,因此在语义分析阶段需要进行类型检查,确保每个变量都被正确地声明为整型或实型。 2.符号表管理:符号表是编译器中的一个重要数据结构,它用于存储变量名和其对应的类型信息。在语义分析阶段,需要建立符号表,并在程序中遇到变量时,将其加入符号表中。 3.常量表管理:TINY编译器支持常量,因此需要建立常量表,并在程序中遇到常量时,将其加入常量表中。 4.表达式计算:在TINY编译器中,表达式计算是语义分析的一个重要部分。在计算表达式的值时,需要考虑运算符的优先级和结合性。 5.错误处理:在语义分析阶段,需要对程序进行错误处理,包括未声明的变量、类型不匹配等错误。 符号表的定义与实现: 符号表是编译器中的一个重要数据结构,用于存储变量名和其对应的类型信息。在TINY编译器中,符号表是一个链表结构,每个节点包含变量名、类型和地址等信息。 在TINY编译器中,符号表的定义如下: ``` struct symbol { char *name; // 变量名 int type; // 变量类型,0表示整型,1表示实型 int addr; // 变量地址 struct symbol *next; // 指向下一个节点的指针 }; struct symbol *symtab; // 符号表 ``` 在程序中遇到变量时,需要将其加入符号表中。加入符号表的方法如下: ``` void enter(char *name, int type, int addr) { struct symbol *p; p = (struct symbol *) malloc(sizeof(struct symbol)); p->name = name; p->type = type; p->addr = addr; p->next = symtab; symtab = p; } ``` 类型检查与表达式计算的实现方法: 在TINY编译器中,类型检查和表达式计算是通过递归遍历抽象语法树实现的。在遍历抽象语法树的过程中,对于每个节点,需要根据节点类型进行不同的操作。 例如,对于加法节点,需要先计算左子树的值,再计算右子树的值,最后将两个值相加。在计算的过程中,需要进行类型检查,确保左右子树的值都是整型或实型,否则会报错。 常量表的管理: TINY编译器中的常量表是一个简单的哈希表。在程序中遇到常量时,需要将其加入常量表中。加入常量表的方法如下: ``` #define HASHSIZE 101 struct node { char *name; int value; struct node *next; }; struct node *hashtable[HASHSIZE]; int hash(char *s) { int hashval; for (hashval = 0; *s != '\0'; s++) { hashval = *s + 31 * hashval; } return hashval % HASHSIZE; } struct node *lookup(char *s) { struct node *np; for (np = hashtable[hash(s)]; np != NULL; np = np->next) { if (strcmp(s, np->name) == 0) { return np; } } return NULL; } struct node *install(char *name, int value) { struct node *np; int hashval; if ((np = lookup(name)) == NULL) { np = (struct node *) malloc(sizeof(struct node)); np->name = name; hashval = hash(name); np->next = hashtable[hashval]; hashtable[hashval] = np; } else { free((void *) np->value); } np->value = value; return np; } ``` 测试语义分析器: 在测试语义分析器时,需要编写一些测试程序,并输出符号表和测试结果。例如,下面是一个简单的TINY测试程序: ``` var x, y: int; begin x := 1; y := 2; if x < y then writeln(x + y) else writeln(x - y) end. ``` 运行结果如下: ``` Symbol Table ----------- Name Type Address ---- ---- ------- x int 0 y int 4 Output ------ -1 ``` 从输出结果可以看出,符号表中包含变量x和y的信息,测试结果为-1,符合预期。 总结: TINY编译器的语义分析实现比较简单,但也涉及到了一些重要的概念和技术,例如符号表、抽象语法树和哈希表等。通过学习TINY编译器的语义分析实现,可以更好地理解编译原理中的一些基本概念和技术。同时,也可以通过编写测试程序来加深对语义分析的理解和掌握。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值