编译[7]引入函数

35 阅读9分钟

概述

上一篇把整个编译器的内部逻辑修改了一遍,修改后,貌似功能和使用表现上并没有什么变化,但那只是对于之前的功能而言。上次修改的目的在于后续的扩展,这一篇,我们要让我们的编程语言支持函数的定义和调用。

动手

完整代码在这里

首先修改类型定义

修改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方法,这些逻辑都运行正常。

运行效果录屏:

use-function录屏.gif

总结

本次给解析器,或者说给这个语言添加了函数的定义和调用,为此增加了不少方法。下一篇将会再完善一下变量的声明,使用let关键字来声明一个变量,而不是任何时候调用一个新变量就会直接声明。目前这种变量声明处理让代码更容易出问题,比如我声明并使用了一个变量叫bad,后续还想继续用这个变量的时候写错了,写成了bed,但代码正常运行,还会多出一个新变量。通过使用固定的语句来生面新变量,后续如果使用了没有声明过的变量就会报错,这样就能避免一些潜在问题。