动手实现编译器(六)——实现全局变量_判定变量是否为全局变量在编译器哪一步-程序员宅基地

技术标签: 编译器  

我们在上一节中实现了对语句的编译,在这一节中,我们希望向语句中加入全局变量。实现类似以下语句:

int a;
int b;
int c;
int d;
int e;
int f;
int g;
a = 2;
b = 3;
c = 4;
d = 5;
e = 6;
f = 7;
g = 8;
print a + b * c / d % e - f + g;

这需要变量有以下功能:

  • 声明变量
  • 使用变量获取存储值
  • 给变量赋值

相关语法定义

变量声明: VarDecl → ‘int’ Ident ‘;’
变量赋值: Stmt → Ident ‘=’ IntConst ‘;’

全局变量符号表

为了实现变量,我们必须要有一张符号表,来存放变量。在本节中,只建立全局变量符号表。

#define SYMBOL_NUM   1024	// 符号表长度
// 符号表结构体
struct symtable
{
    
    char *name;			        // 符号名
};
struct symbol_table Tsym[SYMBOL_NUM];	// 全局符号表

此外,我们还要添加三个操作全局变量符号表的函数。

int Globals = 0;		// 全局符号表下一个空闲位置

// 检查符号s是否在全局符号表中。
// 返回其插槽位置或-1
int find_global(char *s)
{
    
    int i;
    for (i = 0; i < Globals; i++)
    {
    
        if (*s == *Tsym[i].name && !strcmp(s, Tsym[i].name))
        return i;
    }
    return -1;
}

// 获取新的全局符号槽的位置
int new_global()
{
    
    int p;
    if ((p = Globals++) >= SYMBOL_NUM)
    {
    
        fprintf(stderr, "Too many global symbols on line %d\n", Line);
        exit(1);
    }
    return p;
}

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

修改词法分析器

为了实现新加的语法功能,我们需要一些新单词类型:

  • ‘int’,称为 T_KEYINT
  • ‘=’,称为 T_EQU
  • 标识符名称,称为 T_IDENT

将‘int’加入match_keyword()函数:

        case 'i':
            if(!strcmp(s, "int"))
                return (T_KEYINT);
            break;

将"="加入scan()函数:

        case '=':   t->token = T_EQU;   break;

对于标识符,我们已经在scan_ident() 将单词存储到Text变量中了,如果变量不是关键字,那它就是标识符,返回T_IDENT。将scan()函数中的语句段

	                    printf("Unrecognised symbol %s on line %d\n", Text, Line);
	                    exit(1);

修改为

	                    t->token = T_IDENT;
                        break;

修改语句分析器

新增变量语法后,语句分析器也要进行相应的修改:

// 分析语句
void statements()
{
    
    while (1)
    {
    
        switch (Token.token)
        {
    
            case T_PRINT:   print_statement();      break;
            case T_KEYINT:  var_declaration();      break;
            case T_IDENT:   assignment_statement(); break;
            case T_EOF:     return;
            default:    fprintf(stderr, "Syntax error, token:%s on line %d\n", Token.token, Line); 
                        exit(1);
        }
    }
}

同时把原来在statement()函数中的分析打印语句的语句块函数化,生成一个新的函数。

// 分析打印语句
void print_statement()
{
    
    struct ASTnode *tree;
    int reg;
    // 匹配第一个"print"单词
    match(T_PRINT, "print");
    // 分析表达式并生成汇编代码
    tree = binexpr(0);
    reg = code_generator(tree, -1);
    arm_print_reg(reg);
    arm_freeall_registers();
    // 匹配接下来的";"
    semi();
}

声明变量

为词法分析器,添加一个声明变量的函数

// 检查当前单词是否为标识符,并获取下一个单词
void ident()
{
    
    match(T_IDENT, "identifier");
}

// decl.c

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

赋值功能

我们考虑下面这个式子

a = b + c;

我们计算这个式子的流程是先计算b + c,再将和赋给a。所以,在建立AST树的时候,我们要生成一个A_ASSIGN的新类型节点,表示是赋值操作,它的左孩子是表达式(a + b),右孩子是一个A_LVIDENT的新类型的节点。A_LVIDENT节点表示,该节点是左值标识符,左值是绑定到特定位置的值,在这里它是内存中保存变量值的地址。
这样做时,我们将先计算左子树的表达式值,然后赋给右子树中保存的地址得到的标识符。好处是右值不依赖于特定的位置,表达式结果可能在任意寄存器中。
修改AST节点类型为

// AST节点类型
enum
{
    
    A_ADD, A_SUB, A_MUL, A_DIV, A_MOD, A_INT, A_IDENT, A_ASSIGN, A_LVIDENT
};

现在我们需要在 A_INTLIT AST 节点中存储整数文字值,或者在 A_IDENT AST 节点中存储符号的详细信息,所以在 AST 结构中添加了一个并集以执行此操作,修改AST结构体为

// 抽象语法树结构体
struct ASTnode
{
    
    int op;				        // 节点的操作类型
    struct ASTnode *left;
    struct ASTnode *right;
    union {
    
        int intvalue;		    // 对于立即数,储存数值
        int id;			        // 对于标识符,储存符号表位置
    } v;
};

增加分析赋值语句的函数

// 分析赋值语句
void assignment_statement()
{
    
    struct ASTnode *left, *right, *tree;
    int id;
    // 检查标识符
    ident();
    // 检查它是否已定义,然后为它创建一个叶节点
    if ((id = find_global(Text)) == -1)
    {
    
        fprintf(stderr, "Undeclared variable on line %d\n", Line); 
        exit(1);
    }
    right = mkastleaf(A_LVIDENT, id);
    // 匹配等号
    match(T_EQU, "=");
    // 分析接下来的表达式
    left = binexpr(0);
    
    // 生成赋值AST树
    tree = mkastnode(A_ASSIGN, left, right, 0);
    // 生成汇编代码
    code_generator(tree, -1);
    arm_freeall_registers();
    // 匹配";"
    semi();
}

修改代码生成函数

我们首先执行左侧的AST子级,然后获取一个保存左侧子树值的寄存器号,现在我们将此寄存器号传递给右侧子树,我们需要对A_LVIDENT节点执行此操作,以便于让后面的函数知道哪个寄存器保存赋值表达式的右值结果。
代码如下:

// 给定AST,生成汇编代码,返回值为结果所在寄存器号
int code_generator(struct ASTnode *n, int reg)
{
    
    int leftreg, rightreg;
    if (n->left)    leftreg = code_generator(n->left, -1);
    if (n->right)   rightreg = code_generator(n->right, leftreg);
    switch (n->op)
    {
    
        case A_ADD:    return (arm_add(leftreg,rightreg));
        case A_SUB:    return (arm_sub(leftreg,rightreg));
        case A_MUL:    return (arm_mul(leftreg,rightreg));
        case A_DIV:    return (arm_div(leftreg,rightreg));
        case A_MOD:    return (arm_mod(leftreg,rightreg));
        case A_INT:    return (arm_load_int(n->v.intvalue));
        case A_IDENT:  return (arm_load_global(n->v.id));
        case A_LVIDENT:return (arm_stor_global(reg, n->v.id));
        case A_ASSIGN: return rightreg;
        default:    fprintf(stderr, "Unknown AST operator %d\n", n->op);
                    exit(1);
    }
}

注:这里将arm_load()修改成其他arm_load函数一样的arm_load_XXX()形式。
这里解释一下这段代码的意思,考虑以下 AST 树:

           A_ASSIGN
          /        \
       A_INT   A_LVIDENT
        (3)        (5)

我们调用leftreg = code_generator(n->left, -1); 以执行 A_INT操作,这将执行 case A_INT: return (arm_load_int(n->v.intvalue)); 即加载值为3的寄存器并返回寄存器ID。
然后我们调用 rightreg = code_generator(n->right, leftreg); 以执行A_LVIDENT操作,这将把 return (arm_load_global(Tsym[n->v.id].name)); 返回的寄存器存储到名称为Tsym[5]的变量中。
然后我们回到A_ASSIGN操作,右值仍在寄存器中,因此让它保留在那里并返回它。

表达式加载变量

我们对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:
            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;
}

生成汇编代码

上面已经说了,我将旧arm_load()函数的名称更改为arm_load_int(),现在我们有一个函数可以从全局变量加载值:

// 确定变量与.L2标签的偏移量
void set_var_offset(int id)
{
    
    fprintf(Outfile, "\tldr\tr3, .L2+%d\n", id * 4);
}

// 将变量中的值加载到寄存器中,并返回寄存器编号
int arm_load_global(int id)
{
    
    // 获得一个新的寄存器
    int r = arm_alloc_register();
    // 获得变量偏移地址
    set_var_offset(id);
    fprintf(Outfile, "\tldr\tr%d, [r3]\n", r);
    return r;
}

同样我们需要一个函数来将寄存器值保存到变量中:

// 将寄存器的值装载入变量
int arm_stor_global(int r, int id)
{
    
    // 获得变量偏移地址
    set_var_offset(id);
    fprintf(Outfile, "\tstr\tr%d, [r3]\n", r);
    return r;
}

我们还需要一个函数来创建新的全局整数变量:

// 生成全局变量符号表
void arm_global_sym(char *name)
{
    
    fprintf(Outfile, "\t.text\n\t.comm\t%s,4,4\n", name);
}

对应的,我们也要修改汇编尾代码函数,时期能输出全局变量表。

// 汇编尾代码
void arm_postamble()
{
    
    fputs(
        "\tmov     r3, #0\n"
        "\tmov     r0, r3\n"
        "\tpop     {fp, pc}\n"
        ".L3:\n"
        "\t.word   .LC0\n"
        "\t.size   main, .-main\n",
    Outfile);
    fprintf(Outfile, ".L2:\n");
    for (int i = 0; i < Globals; i++)
    {
    
        fprintf(Outfile, "\t.word %s\n", Tsym[i].name);
    }
}

测试结果

输入:

int a;
int b;
int c;
int d;
int e;
int f;
int g;
int h;
int i;
a = 1;
b = 4;
c = 13;
d = 11;
e = 2;
f = 0;
g = 20;
h = 1024;
i = 10;
a = a + b;
c = c - d;
e = e * f;
g = g / g;
h = h % i;
print a;
print c;
print e;
print g;
print h;
g = a + c + e + g + h;
print g;

输出(out.s):

	.text
	.global __aeabi_idiv
	.section	.rodata
	.align  2
.LC0:
	.ascii  "%d\012\000"
	.text
	.align  2
	.global main
	.type   main, %function
main:
	push    {
    fp, lr}
	add     fp, sp, #4
	.text
	.comm	a,4,4
	.text
	.comm	b,4,4
	.text
	.comm	c,4,4
	.text
	.comm	d,4,4
	.text
	.comm	e,4,4
	.text
	.comm	f,4,4
	.text
	.comm	g,4,4
	.text
	.comm	h,4,4
	.text
	.comm	i,4,4
	mov	r4, #1
	ldr	r3, .L2+0
	str	r4, [r3]
	mov	r4, #4
	ldr	r3, .L2+4
	str	r4, [r3]
	mov	r4, #13
	ldr	r3, .L2+8
	str	r4, [r3]
	mov	r4, #11
	ldr	r3, .L2+12
	str	r4, [r3]
	mov	r4, #2
	ldr	r3, .L2+16
	str	r4, [r3]
	mov	r4, #0
	ldr	r3, .L2+20
	str	r4, [r3]
	mov	r4, #20
	ldr	r3, .L2+24
	str	r4, [r3]
	mov	r4, #1024
	ldr	r3, .L2+28
	str	r4, [r3]
	mov	r4, #10
	ldr	r3, .L2+32
	str	r4, [r3]
	ldr	r3, .L2+0
	ldr	r4, [r3]
	ldr	r3, .L2+4
	ldr	r5, [r3]
	add	r4, r4, r5
	ldr	r3, .L2+0
	str	r4, [r3]
	ldr	r3, .L2+8
	ldr	r4, [r3]
	ldr	r3, .L2+12
	ldr	r5, [r3]
	sub	r4, r4, r5
	ldr	r3, .L2+8
	str	r4, [r3]
	ldr	r3, .L2+16
	ldr	r4, [r3]
	ldr	r3, .L2+20
	ldr	r5, [r3]
	mul	r4, r4, r5
	ldr	r3, .L2+16
	str	r4, [r3]
	ldr	r3, .L2+24
	ldr	r4, [r3]
	ldr	r3, .L2+24
	ldr	r5, [r3]
	mov	r0, r4
	mov	r1, r5
	bl	__aeabi_idiv
	mov	r4, r0
	ldr	r3, .L2+24
	str	r4, [r3]
	ldr	r3, .L2+28
	ldr	r4, [r3]
	ldr	r3, .L2+32
	ldr	r5, [r3]
	mov	r6, r4
	mov	r0, r6
	mov	r1, r5
	bl	__aeabi_idiv
	mov	r6, r0
	mul	r6, r6, r5
	sub	r4, r4, r6
	ldr	r3, .L2+28
	str	r4, [r3]
	ldr	r3, .L2+0
	ldr	r4, [r3]
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	ldr	r3, .L2+8
	ldr	r4, [r3]
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	ldr	r3, .L2+16
	ldr	r4, [r3]
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	ldr	r3, .L2+24
	ldr	r4, [r3]
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	ldr	r3, .L2+28
	ldr	r4, [r3]
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	ldr	r3, .L2+0
	ldr	r4, [r3]
	ldr	r3, .L2+8
	ldr	r5, [r3]
	add	r4, r4, r5
	ldr	r3, .L2+16
	ldr	r5, [r3]
	add	r4, r4, r5
	ldr	r3, .L2+24
	ldr	r5, [r3]
	add	r4, r4, r5
	ldr	r3, .L2+28
	ldr	r5, [r3]
	add	r4, r4, r5
	ldr	r3, .L2+24
	str	r4, [r3]
	ldr	r3, .L2+24
	ldr	r4, [r3]
	mov     r1, r4
	ldr     r0, .L3
	bl      printf
	mov     r3, #0
	mov     r0, r3
	pop     {
    fp, pc}
.L3:
	.word   .LC0
	.size   main, .-main
.L2:
	.word a
	.word b
	.word c
	.word d
	.word e
	.word f
	.word g
	.word h
	.word i

输出(out):

5
2
0
1
4
12

总结

我们写出了符号表管理,我们处理了两种新的语句类型,我们添加了一些新单词类型的和一些新的AST节点类型,我们添加了一些代码以生成正确的汇编代码。在下一节中,我们将仿照本节的方法,添加六个比较运算符。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_34851605/article/details/117921750

智能推荐

leetcode 172. 阶乘后的零-程序员宅基地

文章浏览阅读63次。题目给定一个整数 n,返回 n! 结果尾数中零的数量。解题思路每个0都是由2 * 5得来的,相当于要求n!分解成质因子后2 * 5的数目,由于n中2的数目肯定是要大于5的数目,所以我们只需要求出n!中5的数目。C++代码class Solution {public: int trailingZeroes(int n) { ...

Day15-【Java SE进阶】IO流(一):File、IO流概述、File文件对象的创建、字节输入输出流FileInputStream FileoutputStream、释放资源。_outputstream释放-程序员宅基地

文章浏览阅读992次,点赞27次,收藏15次。UTF-8是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。文件字节输入流:每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1。注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码。定义一个与文件一样大的字节数组,一次性读取完文件的全部字节。UTF-8字符集:汉字占3个字节,英文、数字占1个字节。GBK字符集:汉字占2个字节,英文、数字占1个字节。GBK规定:汉字的第一个字节的第一位必须是1。_outputstream释放

jeecgboot重新登录_jeecg 登录自动退出-程序员宅基地

文章浏览阅读1.8k次,点赞3次,收藏3次。解决jeecgboot每次登录进去都会弹出请重新登录问题,在utils文件下找到request.js文件注释这段代码即可_jeecg 登录自动退出

数据中心供配电系统负荷计算实例分析-程序员宅基地

文章浏览阅读3.4k次。我国目前普遍采用需要系数法和二项式系数法确定用电设备的负荷,其中需要系数法是国际上普遍采用的确定计算负荷的方法,最为简便;而二项式系数法在确定设备台数较少且各台设备容量差..._数据中心用电负荷统计变压器

HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板_网页设计成品百度网盘-程序员宅基地

文章浏览阅读7k次,点赞4次,收藏46次。HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 明星、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 军事、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他 等网页设计题目, A+水平作业_网页设计成品百度网盘

【Jailhouse 文章】Look Mum, no VM Exits_jailhouse sr-iov-程序员宅基地

文章浏览阅读392次。jailhouse 文章翻译,Look Mum, no VM Exits!_jailhouse sr-iov

随便推点

chatgpt赋能python:Python怎么删除文件中的某一行_python 删除文件特定几行-程序员宅基地

文章浏览阅读751次。本文由chatgpt生成,文章没有在chatgpt生成的基础上进行任何的修改。以上只是chatgpt能力的冰山一角。作为通用的Aigc大模型,只是展现它原本的实力。对于颠覆工作方式的ChatGPT,应该选择拥抱而不是抗拒,未来属于“会用”AI的人。AI职场汇报智能办公文案写作效率提升教程 专注于AI+职场+办公方向。下图是课程的整体大纲下图是AI职场汇报智能办公文案写作效率提升教程中用到的ai工具。_python 删除文件特定几行

Java过滤特殊字符的正则表达式_java正则表达式过滤特殊字符-程序员宅基地

文章浏览阅读2.1k次。【代码】Java过滤特殊字符的正则表达式。_java正则表达式过滤特殊字符

CSS中设置背景的7个属性及简写background注意点_background设置背景图片-程序员宅基地

文章浏览阅读5.7k次,点赞4次,收藏17次。css中背景的设置至关重要,也是一个难点,因为属性众多,对应的属性值也比较多,这里详细的列举了背景相关的7个属性及对应的属性值,并附上演示代码,后期要用的话,可以随时查看,那我们坐稳开车了······1: background-color 设置背景颜色2:background-image来设置背景图片- 语法:background-image:url(相对路径);-可以同时为一个元素指定背景颜色和背景图片,这样背景颜色将会作为背景图片的底色,一般情况下设置背景..._background设置背景图片

Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程-程序员宅基地

文章浏览阅读2.6k次,点赞2次,收藏8次。Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程

PyCharm2021安装教程-程序员宅基地

文章浏览阅读10w+次,点赞653次,收藏3k次。Windows安装pycharm教程新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入下载安装PyCharm1、进入官网PyCharm的下载地址:http://www.jetbrains.com/pycharm/downl_pycharm2021

《跨境电商——速卖通搜索排名规则解析与SEO技术》一一1.1 初识速卖通的搜索引擎...-程序员宅基地

文章浏览阅读835次。本节书摘来自异步社区出版社《跨境电商——速卖通搜索排名规则解析与SEO技术》一书中的第1章,第1.1节,作者: 冯晓宁,更多章节内容可以访问云栖社区“异步社区”公众号查看。1.1 初识速卖通的搜索引擎1.1.1 初识速卖通搜索作为速卖通卖家都应该知道,速卖通经常被视为“国际版的淘宝”。那么请想一下,普通消费者在淘宝网上购买商品的时候,他的行为应该..._跨境电商 速卖通搜索排名规则解析与seo技术 pdf

推荐文章

热门文章

相关标签