概述
上一篇把整个编译器的内部逻辑修改了一遍,修改后,貌似功能和使用表现上并没有什么变化,但那只是对于之前的功能而言。上次修改的目的在于后续的扩展,这一篇,我们要让我们的编程语言支持函数的定义和调用。
动手
首先修改类型定义
修改nl.h文件,修改后完整代码在这里。
之前值的类型只有整型和浮点型,现在要加一个空类型,因为调用函数需要有返回值,但函数也可以不返回值,不返回值时定义返回的是空类型的值,修改枚举类型ValueType,多加一个空类型值:
typedef enum {
INT_VALUE = 1,
DOUBLE_VALUE,
NULL_VALUE // 多加这个
} ValueType;
表达式类型多增加一种函数调用表达式类型,修改ExpressionType这个枚举类型,多加一个函数调用表达式类型:
typedef enum {
INT_EXPRESSION = 1,
DOUBLE_EXPRESSION,
ADD_EXPRESSION,
SUB_EXPRESSION,
MUL_EXPRESSION,
DIV_EXPRESSION,
MOD_EXPRESSION,
VARIABLE_EXPRESSION,
MINUS_EXPRESSION,
FUNCTION_CALL_EXPRESSION, // 多加这个
EXPRESSION_TYPE_PLUS
} ExpressionType;
增加函数定义语句结构体,用于描述函数定义语句需要包含的信息,定义一个函数需要直到函数名以及函数包含的语句,所有的语句都放在语句块里面,所以还需要定义块结构,当前的函数不支持传参,所以还不需要记录参数。
typedef struct Block_tag Block; // 块结构具体定义在后面
typedef struct { // 函数定义语句
char *identifier;
Block *block;
} FunctionDefinitionStatement;
增加两种语句类型,函数定义语句以及return语句,函数需要有return语句用来返回。
typedef enum {
EXPRESSION_STATEMENT = 1,
ASSIGN_STATEMENT,
PRINT_STATEMENT,
FUNCTION_DEFINITION_STATEMENT, // 函数定义语句
RETURN_STATEMENT, // return语句
STATEMENT_TYPE_PLUS
} StatementType;
语句结构体的联合体值加入语句定义结构,至于return语句,复用原来的expression字段存放即可:
typedef struct {
StatementType type;
union {
Expression *expression;
AssignStatement assign;
FunctionDefinitionStatement functionDefinition; // 函数定义语句
} u;
} Statement;
加上块的具体定义,块里面放的就是语句列表:
struct Block_tag {
StatementList *statement_list;
};
每次执行完函数定义语句之后,需要记录下来,这样调用的时候才能查找到并且调用,记录函数定义需要另外的结构体,存储函数以链表的方式记录下所有函数,所以除了记录函数名和语句块之外,还需要一个指针指向下一个:
typedef struct FunctionDefinition_tag {
char *name;
Block *block;
struct FunctionDefinition_tag *next;
} FunctionDefinition;
接下来会让所有语句的执行都返回值,普通语句执行返回空值,return语句执行的返回值看实际情况而定,这里先定义语句的值类型,需要记录值的类型以及具体值是什么:
typedef enum { // 定义类型
NORMAL_STATEMENT_VALUE = 1,
RETURN_STATEMENT_VALUE,
STATEMENT_VALUE_PLUS
} StatementValueType;
typedef struct { // 语句值结构体
StatementValueType type; // 类型
NL_Value return_value; // 具体值
} StatementValue;
King上面多加全局的函数定义列表:
struct King_tag {
Variable *variable;
StatementList *statement_list;
FunctionDefinition *function_list; // 函数列表
};
上面是各新类型的定义,下面还要添加各个新方法的定义,不一一列在这里,因为下面还要介绍所有新增方法。
增加新的创建方法
修改create.c文件,修改后的完整代码在这里。
经过上一篇的改造后,语法解释时遇到所有表达式都会创建相应类型的表达式,要支持函数调用,函数调用也是一种表达式,所以需要创建函数调用表达式,nl_alloc_expression用于分配表达式结构体盏空间,同时只需要记录函数名即可:
Expression *
nl_create_function_call_expression(char *identifier) {
Expression *result = nl_alloc_expression(FUNCTION_CALL_EXPRESSION);
result->u.identifier = identifier;
return result;
}
添加创建函数定义语句的方法,函数定义需要记录函数名,以及函数内包含的语句块,同时,还要添加创建return语句的方法,return语句只要知道return的表达式即可:
Statement *
nl_create_function_definition_statement(char *identifier, Block *block) {
Statement *statement = malloc_statement(FUNCTION_DEFINITION_STATEMENT);
statement->u.functionDefinition.identifier = identifier;
statement->u.functionDefinition.block = block;
return statement;
}
Statement *
nl_create_return_statement(Expression *expression) {
Statement *statement = malloc_statement(RETURN_STATEMENT);
statement->u.expression = expression;
return statement;
}
添加创建语句块的方法,语句块记录的是语句列表:
Block *
nl_create_block(StatementList *statement_list) {
Block *block = malloc(sizeof(Block));
block->statement_list = statement_list;
return block;
}
改造已有执行方法以及添加新执行方法
修改execute.c文件,修改后的完整内容在这里。
所有语句的执行方法以及语句列表的执行方法都需要有返回值,因为函数执行需要返回值,就需要函数内的语句块和语句列表的执行有返回值,进而也需要语句执行有返回值。
已有的nl_execute_expression_statement方法,定义返回值,返回类型默认是普通:
StatementValue
nl_execute_expression_statement(Statement *statement) {
StatementValue sValue;
sValue.type = NORMAL_STATEMENT_VALUE;
nl_eval_expression(statement->u.expression);
return sValue;
}
同样,nl_execute_assign_statement,nl_execute_print_statement这些方法,也加上返回值类型,并定义返回值,类型也是普通,并直接返回。
添加函数定义语句的执行方法,执行的是一个语句,所以参数还是语句,下面注释说明每一行的用意。当前只有全局函数定义,因为目前编译器还没有作用域的功能。
StatementValue
nl_execute_function_definition_statement(Statement *statement) {
King *king;
StatementValue sValue;
FunctionDefinition *newFun;
char *identifier = statement->u.functionDefinition.identifier; // 拿到定义的函数名
Block *block = statement->u.functionDefinition.block; // 拿到函数定义的语句块
FunctionDefinition *functionDefinition = nl_search_function(identifier); // 查找是否有同名函数定义
sValue.type = NORMAL_STATEMENT_VALUE; // 返回值是普通类型
/* 已有同名函数 */
if (functionDefinition) { // 已有同名函数定义,报错
printf("[runtime error] execute function definition statement while function [%s] is exist.\n", identifier);
exit(1);
}
king = nl_get_current_king();
newFun = malloc(sizeof(FunctionDefinition)); // 分配函数定义结构体空间
newFun->block = block; // 写入语句块
newFun->name = identifier; // 写入函数名
// 新定义的函数记录到全局king的属性上。
newFun->next = king->function_list;
king->function_list = newFun;
return sValue;
}
添加return语句的执行方法,返回值类型是return类型,如果return内容是空,返回的值就是空值,否则就是被return的表达式值。
StatementValue
nl_execute_return_statement(Statement *statement) {
StatementValue sValue;
sValue.type = RETURN_STATEMENT_VALUE;
if (!statement->u.expression) {
sValue.return_value.type = NULL_VALUE;
return sValue;
}
sValue.return_value = nl_eval_expression(statement->u.expression);
return sValue;
}
原来的nl_execute_statement方法,要加一个返回值,并且赋值来自每种语句类型的执行返回值,当然还要新增函数定义语句和return语句。
StatementValue
nl_execute_statement(Statement *statement) {
StatementValue sValue;
switch(statement->type) {
case EXPRESSION_STATEMENT: {
sValue = nl_execute_expression_statement(statement);
break;
}
case ASSIGN_STATEMENT: {
sValue = nl_execute_assign_statement(statement);
break;
}
case PRINT_STATEMENT: {
sValue = nl_execute_print_statement(statement);
break;
}
case FUNCTION_DEFINITION_STATEMENT: {
sValue = nl_execute_function_definition_statement(statement);
break;
}
case RETURN_STATEMENT: {
sValue = nl_execute_return_statement(statement);
break;
}
case STATEMENT_TYPE_PLUS:
default: {
printf("[runtime error] execute statement with unexpected type [%d].\n", statement->type);
exit(1);
}
}
return sValue;
}
修改语句列表执行方法nl_execute_statement_list,原来直接执行完所有语句,现在改成要有返回值,如果语句列表是空,则返回普通类型值,循环执行语句列表时,遇到返回值类型是return类型时,不再往下执行。
StatementValue
nl_execute_statement_list(StatementList *list) {
StatementList *now;
StatementValue sValue;
sValue.type = NORMAL_STATEMENT_VALUE;
if (list == NULL) {
return sValue;
}
// 返回值是普通类型则继续往下执行
for (now = list; now && (sValue.type == NORMAL_STATEMENT_VALUE); now = now->next) {
sValue = nl_execute_statement(now->statement);
}
return sValue;
}
修改/添加某些表达式计算方法
修改eval.c文件,修改后的完整内容在这里。
需要计算函数表达式的结果,添加eval_function_call_expression方法,它接收一个表达式作为参数,下面的代码加了注释做说明。
static NL_Value
eval_function_call_expression(Expression *exp) {
NL_Value result;
StatementValue sValue;
char *fun_name = exp->u.identifier; // 拿到函数名
FunctionDefinition *fun_def = nl_search_function(fun_name); // 搜索该函数
if (!fun_def) { // 找不到就报错
printf("[runtime error] eval function call expression, function [%s] is not define.\n", fun_name);
exit(1);
}
sValue = nl_execute_statement_list(fun_def->block->statement_list); // 执行函数的语句块,拿到执行结果
if (sValue.type == RETURN_STATEMENT_VALUE) { // 如果是中途有return语句,这结果就是该return语句的结果
result = sValue.return_value;
} else {
result.type = NULL_VALUE; // 不是return语句则是空类型返回值
}
return result;
}
计算表达式值的方法(eval_expression)要改一下,要考虑表达式为空的情况,比如return语句后面可以跟一个表达式,返回值就是该表达式的值,而返回的可以是空的表达式。另外,switch-case结构要多加函数表达式这种类型的表达式。
static NL_Value
eval_expression(Expression *exp) {
NL_Value v;
if (!exp) { // 加上这个判断
v.type = NULL_VALUE;
return v;
}
switch (exp->type) {
......
case FUNCTION_CALL_EXPRESSION: { // 加上这个case
v = eval_function_call_expression(exp);
break;
}
......
}
return v;
}
函数存放
修改interface.c,修改后的完整文件在这里。
修改NL_create_king方法,创建king时要初始化存放函数定义的链表。
King *
NL_create_king(void) {
King *king = malloc(sizeof(struct King_tag));
king->variable = NULL;
king->statement_list = NULL;
king->function_list = NULL; // 加上这句
return king;
}
添加搜索函数的方法,调用函数时,要先搜索函数是否存在,从king的函数链表上找。
FunctionDefinition *
nl_search_function(char *name) {
FunctionDefinition *pos;
King *king = nl_get_current_king();
for (pos = king->function_list; pos; pos = pos->next) {
if (!strcmp(pos->name, name)) {
break;
}
}
return pos;
}
修改词法
前面把所有相关方法都准备好了,接着修改词法,修改词法文件nl.l,修改后的完整文件内容在这里。
词法增加函数定义必须的以下token:
"function" return FUNCTION;
"return" return RETURN;
"{" return LC;
"}" return RC;
修改语法
修改语法,让解释器能识别新的函数相关语句语法。修改nl.y文件,修改后的完整文件内容在这里。
在%union上加上block类型声明:
%union {
Expression *expression;
char *identifier;
Statement *statement;
StatementList *statement_list;
Block *block; // 加这个
}
加上新的token定义,新的语句类型定义,还定义了语句块类型:
%token ...... FUNCTION LC RC RETURN
%type <statement> ...... function_definition_statement return_statement
%type <block> block
之前的语法产生式中有语句列表statement_list的产生式,而整个代码文件可以看作是一整个包含所有语句的语句列表组成,解析完整个代码文件后,需要产生一个总的语句列表存放在king里面,因为后续执行所有语句就是来自king的语句列表,所以要加一个识别全局语句列表的产生式,当全篇解析完组成一个总的语句列表后,把它放在king的语句列表属性中:
page
:
| statement_list
{
King *king = nl_get_current_king();
king->statement_list = $1;
}
;
语句列表组成过程中,对首个语句,以及中间的子列表的处理逻辑做更改,首个语句需要创建语句列表,后续遇到的语句把它加到前面已经产生的子语句列表中:
statement_list
: statement
{
$$ = nl_create_statement_list($1);
}
| statement_list statement
{
$$ = nl_add_to_statement_list($1, $2);
}
;
增加两种新的语句类型,函数定义语句,return语句:
statement
: expression_statement
| assign_statement
| print_statement
| function_definition_statement
| return_statement
;
function_definition_statement
: FUNCTION IDENTIFIER LP RP block
{
$$ = nl_create_function_definition_statement($2, $5);
}
;
return_statement
: RETURN expression SEMICOLON
{
$$ = nl_create_return_statement($2);
}
| RETURN SEMICOLON
{
$$ = nl_create_return_statement(NULL);
}
;
加上函数表达式的语法,即表达式可以包含函数的调用:
primary_expression
......
| IDENTIFIER LP RP
{
$$ = nl_create_function_call_expression($1);
}
......
;
加上语句块的语法:
block
: LC statement_list RC
{
$$ = nl_create_block($2);
}
| LC RC
{
$$ = nl_create_block(NULL);
}
;
效果
最后写了一段代码,让加上函数功能的解析器去跑:
x1 = 23 + 7;
print(x1);
print(10 * 10);
function f1() {
print(x1);
x2 = 10;
return 20;
x3 = 20;
}
print(-f1() *10);
x1 = x1 - f1();
print(x1);
print(x2);
function f2() {
print(2);
function f3() {
return x1 + 10;
}
return f3() * 2;
}
print(f2() + 3);
仔细读一下这段代码,里面定义了方法f1,而f1里面有调用打印方法,并且f1方法也在之后的表达式中被调用,另外还定义了f2方法,并且在f2方法里面又定义并调用了f3方法,这些逻辑都运行正常。
运行效果录屏:
总结
本次给解析器,或者说给这个语言添加了函数的定义和调用,为此增加了不少方法。下一篇将会再完善一下变量的声明,使用let关键字来声明一个变量,而不是任何时候调用一个新变量就会直接声明。目前这种变量声明处理让代码更容易出问题,比如我声明并使用了一个变量叫bad,后续还想继续用这个变量的时候写错了,写成了bed,但代码正常运行,还会多出一个新变量。通过使用固定的语句来生面新变量,后续如果使用了没有声明过的变量就会报错,这样就能避免一些潜在问题。