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

本篇博客详细介绍了如何在自定义的SysY编程语言中实现函数调用和返回值。通过修改词法分析、语法分析和代码生成器,区分变量和函数调用,处理return语句,以及重构代码以符合新的语法规则。文中还展示了具体的代码示例和测试用例,展示了函数调用和返回的完整流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

  • 定义一个函数
  • 调用一个目前无法使用的单一值的函数
  • 从函数返回一个值
  • 将函数调用用作语句和表达式
  • 确保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

总结

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值