编译[4]使用变量

1,121 阅读6分钟

概述

接着上一篇,继续我们的实现,四则运算语言有了,给它加上使用变量的功能。

动手

完整代码在这里,是另一个分支。

先实现最简单的方式,让我们的语言可以直接使用变量,无需声明,首次给变量赋值就会创建变量,使用未被创建的变量会报错终止程序。准备添加变量定义语句,因为无需声明,所以变量出现时要么是直接被赋值,要么是被使用

a = 78;

b = a + 34;

b + 3 * a;

识别新token

词法分析需要识别标识符,即变量名,也需要识别等号('=')。

在词法分析代码里面加上相应代码,完整文件在这里

识别等号('='),返回新的token类型ASSIGN

识别变量名的正则表达,其实就是字母或下划线开头,后面跟任意字符数字下划线。识别到变量名之后,利用方法(nl_create_identifier)创建一个标识符,yytext存放了识别到的字符串,这里就是变量名,yylval也是一个全局变量,它是个联合体,对应语法分析中yacc文件中%union的定义,下面还会提到,其实就是给联合体多定义了一个叫identifier的成员变量,词法分析这里给这个变量赋值,语法分析时遇到标识符就能从这个变量拿到值,处理完逻辑后也返回了标识符的token类型IDENTIFIER

......
%%
......
"=" return ASSIGN;
[A-Za-z_][A-Za-z_0-9]* {
    yylval.identifier = nl_create_identifier(yytext);
    return IDENTIFIER;
}
......
%%
......

识别新的语句

语法分析需要识别新的语句,可以叫赋值语句,或者叫变量声明语句,反正声明和赋值是一起的。

完整文件在这里

%union多加一个变量identifier存放标识符的值,就是变量名字符串指针。

%union {
    Expression *expression;
    char *identifier;
}

增加新的token类型ASSIGNIDENTIFIER,并且识别到IDENTIFIER时从identifier拿值。

%token SEMICOLON ADD SUB MUL DIV LP RP MOD ASSIGN
%token <identifier> IDENTIFIER

语法规则中,语句除了原来的表达式语句,多加了变量声明语句IDENTIFIER ASSIGN expression SEMICOLON,这个格式很明确,就是标识符(IDENTIFIER)等于(ASSIGN)某个表达式(expression),后面跟个分号(SEMICOLON)。逻辑执行中用到了新加的King类型,他作为全局控制器,存放所有变量,目前所有变量都是全局变量。还使用了新的方法nl_execute_assign_statement,会创建变量和赋值。

statement
    : expression SEMICOLON
    {
        NL_Value v = nl_eval_expression($1);
        nl_print_value(&v);
    }
    | IDENTIFIER ASSIGN expression SEMICOLON
    {
        King *king = nl_get_current_king();
        nl_execute_assign_statement(king, $1, $3);
    }
    ;

标识符的识别加到primary_expression中,这样做为了能在表达式中使用变量,遇到变量的逻辑是调用nl_create_variable_expression方法返回变量表达式,这里面的逻辑下面还会提到,简单说就是去全局控制器king那里找这个变量,再包装成一个表达式。

primary_expression
    : SUB primary_expression
    {
        $$ = nl_create_minus_expression($2);
    }
    | LP expression RP
    {
        $$ = $2;
    }
    | IDENTIFIER
    {
        $$ = nl_create_variable_expression($1);
    }
    | INT_LITERAL
    | DOUBLE_LITERAL
    ;

新加一个类型定义文件_NL.h完整文件在这里

里面放些可以给外部使用的方法变量。其实整个编译器本身的内容都是看做内部,而main方法是外部,它是调用方。文件中定义了新的类型King,创建king的方法(NL_create_king),以及启动编译的方法(NL_compile)。

#include <stdio.h>

typedef struct King_tag King;

King * NL_create_king(void);
void NL_compile(King *king, FILE *fp);

内部用的类型定义文件(nl.h)也需要修改,完整文件在这里

引入新加的类型定义文件(_NL.h)

#include "_NL.h"

定义变量类型和King类型,这里定义的类型是King_tag,在_NL.h里面又多加了King的定义

typedef struct Variable_tag {
    char *name;
    NL_Value value;
    struct Variable_tag *next;    
} Variable;

struct King_tag {
    Variable *variable;
};

另外还声明了一些方法,直接讲每个方法。

create.c增加了方法,完整文件在这里

创建标识符的方法nl_create_identifier,在词法分析中识别出一个变量名后,另外分配空间存放该变量名,并返回对应指针。

char *
nl_create_identifier(char *str) {
    char *new_str;
    new_str = malloc(strlen(str) + 1);
    strcpy(new_str, str);
    return new_str;
}

创建变量表达式,在表达式中遇到变量时,调用该方法,该方法拿到king,获取变量的值,再通过convert_value_to_expression方法把值转成表达式。

Expression *
nl_create_variable_expression(char *identifier) {
    Expression *exp;
    King *king = nl_get_current_king();
    NL_Value v = nl_eval_variable(king, identifier);
    exp = convert_value_to_expression(&v);
    return exp;
}

再看eval.c完整文件在这里

增加了计算变量值的方法,通过直接去king上面查找该变量,拿到它的值返回,查找不到就报错。

NL_Value
nl_eval_variable(King *king, char *identifier) {
    Variable *var = nl_search_global_variable(king, identifier);
    if (var != NULL) {
        return var->value;
    } else {
        printf("[runtime error] This variable[%s] has not been declared.\n", identifier);
        exit(1);
    }
}

加了一个新文件execute.c,放一些执行类的方法,完整文件在这里

目前只有一个方法,用来执行变量赋值语句,就在语法分析遇到变量赋值语句时调用。执行赋值时,先通过nl_eval_expression方法计算出表达式的值,再通过king查找同名变量,如果能找到,说明之前声明过,直接赋新值,如果没找到,就还要给king添加新的变量同时赋值。

void
nl_execute_assign_statement(King *king, char *identifier, Expression *exp) {
    NL_Value v;
    Variable *var;

    v = nl_eval_expression(exp);
    var = nl_search_global_variable(king, identifier);

    if(var != NULL) {
        var->value = v;
    } else {
        nl_add_global_variable(king, identifier, &v);
    }
}

还有一个新文件interface.c,放一些跟king相关的方法,以及对外的方法。

完整文件在这里

声明静态变量current_king,并且配套了get和set方法,还有创建king的方法NL_create_king,分配堆空间,设置静态变量current_king的值,全局变量就放在king的variable属性中,以链表的形式存放。

static King *current_king;

void
nl_set_current_king(King *king) {
    current_king = king;
}

King *
nl_get_current_king(void) {
    return current_king;
}

King *
NL_create_king(void) {
    King *king = malloc(sizeof(struct King_tag));
    king->variable = NULL;

    nl_set_current_king(king);
    return king;
}

还有查找变量(nl_search_global_variable)和增加变量(nl_add_global_variable)的方法,查找变量直接到king存放变量的属性中以链表形式查找,添加变量需要分配变量空间,再插到链表中。

Variable *
nl_search_global_variable(King *king, char *identifier) {
    Variable *result;
    for(result = king->variable; result; result = result->next) {
        if(!strcmp(result->name, identifier)) {
            return result;
        }
    }
    return NULL;
}

void
nl_add_global_variable(King *king, char *identifier, NL_Value *value) {
    Variable *new_var = malloc(sizeof(Variable));
    new_var->name = malloc(strlen(identifier) + 1);
    strcpy(new_var->name, identifier);
    new_var->next = king->variable;
    king->variable = new_var;
    new_var->value = *value;
}

还有启动执行编译的方法(NL_compile),需要设置好源代码的输入指针,fp。本质上也是调用了yyparse方法启动解释执行。

void
NL_compile(King *king, FILE *fp) {
    extern int yyparse(void);
    extern FILE *yyin;

    yyin = fp;
    if (yyparse()) {
        fprintf(stderr, "Error ! \n");
        exit(1);
    }
}

最后看main.c,完整文件在这里

引入对外的声明文件_NL.h,声明了king变量,调用NL_create_king方法创建了king实例,读取文件这个逻辑跟原来一样,最后调用NL_compile启动执行。

#include "_NL.h"

int main(int argc, char **argv) {
    King *king;
    FILE *fp;

    if (argc != 2) {
        fprintf(stderr, "usage:%s filename", argv[0]);
        exit(1);
    }

    fp = fopen(argv[1], "r");
    if (fp == NULL) {
        fprintf(stderr, "%s not found.\n", argv[1]);
        exit(1);
    }

    king = NL_create_king();

    NL_compile(king, fp);

    return 0;
}

运行效果

准备了要跑的测试文件,最后的10 + x中,x未声明就使用,会报错。

abc = 34;
4 + 10 * abc;

_abc = 1+ 1.1;
10 * _abc + abc;

abc = abc * 2;
abc * 2;

10 + x;
x = 10;

录屏看看效果

使用变量 (1).gif

结束

目前运行四则语言时,遇到四则语句就直接计算并打印出结果,打印这件事是它自发的,应该改成由我们来控制,想什么时候打印就什么时候打印,想打印哪个变量就打印哪个变量,下一篇,实现一个打印语句。

进入下一篇