概述
接着上一篇,继续我们的实现,四则运算语言有了,给它加上使用变量的功能。
动手
完整代码在这里,是另一个分支。
先实现最简单的方式,让我们的语言可以直接使用变量,无需声明,首次给变量赋值就会创建变量,使用未被创建的变量会报错终止程序。准备添加变量定义语句,因为无需声明,所以变量出现时要么是直接被赋值,要么是被使用
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类型ASSIGN
和IDENTIFIER
,并且识别到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;
录屏看看效果
结束
目前运行四则语言时,遇到四则语句就直接计算并打印出结果,打印这件事是它自发的,应该改成由我们来控制,想什么时候打印就什么时候打印,想打印哪个变量就打印哪个变量,下一篇,实现一个打印语句。