编译[6]先解析后执行

683 阅读11分钟

概述

上一篇其实只做了一个小小改动,增加了打印方法,本质上没改变什么。当前实现的解析器,它一边读入字符,一边分析,碰到一个语句就执行一个,这种方式往后难以扩展。比如引入函数,定义函数,函数的内容也是语句,解析函数定义时肯定不能执行其中的语句,函数内的语句只能在函数调用时才执行,并且还跟函数调用时的上下文有关。这一篇,我们要做的就是改造解析执行流程,解析的过程只解析,把解析的内容保存下来,最后再执行所有内容。

动手

完整代码在这里

修改类型定义

修改nl.h,修改后的完整代码在这里

枚举类型ExpressionType记录了所有表达式类型,现在要加上变量表达式(VARIABLE_EXPRESSION),和负数表达式(MINUS_EXPRESSION)。

因为原来是边解析边计算,遇到取负数时,被取负数的那个数肯定已经算出来了,直接给它取负数即可,但现在不能一边解析一边计算了,被取负数的对象可能是一个负载的表达式,当前你不知道它的值,你只能先记录这个表达式,后续执行时,计算出这个表达式的值再取负数。

同理,变量表达式记录了一个变量,解析时还不知道这个变量的值,特别是在函数中时,函数未调用前,都不确定里面的变量会变成什么值。

加上后,定义变成这样:

typedef enum {
    INT_EXPRESSION = 1,
    DOUBLE_EXPRESSION,
    ADD_EXPRESSION,
    SUB_EXPRESSION,
    MUL_EXPRESSION,
    DIV_EXPRESSION,
    MOD_EXPRESSION,
    VARIABLE_EXPRESSION,
    MINUS_EXPRESSION,
    EXPRESSION_TYPE_PLUS
} ExpressionType;

加一种二元表达式取值类型,加减乘除这些表达式都是二元表达式,对于二元表达式,操作符左右的操作数也依然是表达式,需要记录起来。

typedef struct {
    Expression *left;
    Expression *right;
} BinaryExpression;

表达式定义结构体Expression_tag,之前它的取值只有整数或者浮点数,定义成下面这样,u是一个联合体,根据type不同,u取不同值,之前都是直接计算出每个表达式的结果,所以只需要记录整数或者浮点数即可。

struct Expression_tag {
    ExpressionType type;
    union {
        int int_value;
        double double_value;
    } u;
};

改成解析阶段不计算结果,解析时不同表达式记录关键信息,变量表达式需要记录变量名,负数表达式需要记录被取负值的表达式,执行时能计算该表达式的值,然后取负得到最终结果。而所有二元表达式需构造上面定义的BinaryExpression结构体来记录,修改后如下:

struct Expression_tag {
    ExpressionType type;
    union {
        int int_value;
        double double_value;
        char *identifier;
        Expression *minus_expression;
        BinaryExpression binary_expression;
    } u;
};

各种语句也需在解析过程一一记录。之前并没有使用语句类型结构体,因为之前解析到表达式直接得出计算结果,还没用到语句类型的结构体,而赋值语句也是解析到之后就创建变量或直接赋值,现在解析到一个语句后不做执行计算,当然要先记录下来,在执行阶段取执行。

给语句类型枚举StatementType加上赋值语句类型(ASSIGN_STATEMENT)和打印语句类型(PRINT_STATEMENT):

typedef enum {
    EXPRESSION_STATEMENT = 1,
    ASSIGN_STATEMENT,
    PRINT_STATEMENT,
    STATEMENT_TYPE_PLUS
} StatementType;

定义赋值语句结构体,赋值语句关键信息是被赋值的变量名,以及值来源的表达式。

typedef struct {
    char *identifier;
    Expression *expression;
} AssignStatement;

添加语句结构体,type记录是什么类型语句,u是一个联合体记录关键信息。表达式语句和打印语句只需要记录目标语句即可,当前的打印语句还比较简单,赋值语句就用刚新加的赋值语句结构体记录。

typedef struct {
    StatementType type;
    union {
        Expression *expression;
        AssignStatement assign;
    } u;
} Statement;

有了单个语句,还需要有语句列表,表示一连串语句,这里用链表来表示,简单粗暴。

typedef struct StatementList_tag {
    Statement *statement;
    struct StatementList_tag *next;
} StatementList;

修改一下全局控制变量king的结构体定义,多加了语句列表的记录,最终解析完所有语句都记录在这上面。

struct King_tag {
    Variable *variable;
    StatementList *statement_list;
};

还有一些新方法的定义,都添加到这个文件,下面会讲到。

增加/修改创建方法

修改create.c,改变个别已有方法,添加新的方法。完整文件内容在这里

之前已有的nl_create_variable_expression方法用于创建一个变量表达式,它的逻辑是直接找king拿到那个变量的值,拿到的是整数或浮点数值,然后再转成表达式返回,所以实际拿到的就是一个整数或浮点数表达式。要解析和执行分开,这里要创建一个真正的变量表达式,记录关键的变量名即可。

Expression *
nl_create_variable_expression(char *identifier) {
    Expression *exp;
    exp = nl_alloc_expression(VARIABLE_EXPRESSION);
    exp->u.identifier = identifier;
    return exp;
}

修改创建二元表达式的方法nl_create_binary_expression,原来该方法直接拿到两个操作数leftright计算出值,返回整数/浮点数表达式。

新逻辑先判断两个操作数的类型,如果两者都是现成的整数或浮点数,可以直接计算结果返回整数/浮点数表达式,比如操作数都是数字常量,则解析阶段就可以直接计算结果。

若两个操作数有一个或以上是别的类型表达式,意味着当前还不知道值,就需要直接把左右表达式记录下来,用表达式记录值的联合体中的binary_expression来记录。

Expression *
nl_create_binary_expression(ExpressionType type, Expression *left, Expression *right) {
    if ((left->type == INT_EXPRESSION || left->type == DOUBLE_EXPRESSION)
        && (right->type == INT_EXPRESSION || right->type == DOUBLE_EXPRESSION)) {
        NL_Value v;
        v = nl_eval_binary_expression(type, left, right);

        left = convert_value_to_expression(&v);
        return left;
    } else {
        Expression *exp;
        exp = nl_alloc_expression(type);
        exp->u.binary_expression.left = left;
        exp->u.binary_expression.right = right;
        return exp;
    }
    
}

修改负数表达式,若目标表达式类型是整数或浮点数,可用原来的逻辑,直接计算取负数的结果返回,否则就要分配空间创建负数表达式类型的表达式,记录目标表达式。

Expression *
nl_create_minus_expression(Expression *exp) {
    Expression *result;
    if (exp->type == INT_EXPRESSION) {
        exp->u.int_value = -exp->u.int_value;
        result = exp;
    } else if (exp->type == DOUBLE_EXPRESSION) {
        exp->u.double_value = -exp->u.double_value;
        result = exp;
    } else {
        result = nl_alloc_expression(MINUS_EXPRESSION);
        result->u.minus_expression = exp;
    }
    return result;
}

接着还要添加一系列语句相关的创建方法。

分配空间创建语句结构体并指定语句类型的方法:

Statement *
malloc_statement(StatementType type) {
    Statement *statement = malloc(sizeof(Statement));
    statement->type = type;
    return statement;
}

创建表达式语句的方法:

Statement *
nl_create_expression_statement(Expression *expression) {
    Statement *statement = malloc_statement(EXPRESSION_STATEMENT);
    statement->u.expression = expression;
    return statement;
}

创建赋值语句和打印语句的方法,用到新加的赋值语句类型ASSIGN_STATEMENT和打印语句类型PRINT_STATEMENT。而赋值语句用到了Statement结构体中u联合体新加的assign成员。

Statement *
nl_create_assign_statement(char *identifier, Expression *expression) {
    Statement *statement = malloc_statement(ASSIGN_STATEMENT);
    statement->u.assign.identifier = identifier;
    statement->u.assign.expression = expression;
    return statement;
}

Statement *
nl_create_print_statement(Expression *expression) {
    Statement *statement = malloc_statement(PRINT_STATEMENT);
    statement->u.expression = expression;
    return statement;
}

创建语句列表的方法:

StatementList *
nl_create_statement_list(Statement *statement) {
    StatementList *statement_list = malloc(sizeof(StatementList));
    statement_list->statement = statement;
    statement_list->next = NULL;
    return statement_list;
}

把语句添加到语句列表的方法,逻辑是先判断列表是否为空,若为空,则创建语句列表,第一个语句就是要添加的语句。若已有语句列表,就遍历它,知道最后一个,然后把新的语句插到最后:

StatementList *
nl_add_to_statement_list(StatementList *list, Statement *statement) {
    StatementList *now;
    if (list == NULL) {
        return nl_create_statement_list(statement);
    }
    for (now = list; now->next; now = now->next)
        ;
    
    now->next = nl_create_statement_list(statement);

    return list;
}

计算表达式值

修改eval.c文件,修改后完整文件内容在这里

添加变量表达式求值方法,之前已有的变量求值方法nl_eval_variable要删掉,变量求值改成了变量表达式求值,因为解析阶段都只记录各种表达式,执行阶段需要对各种表达式求值。从变量表达式拿到变量名,让king搜索该变量,能找到该变量则返回值,找不到则报错。

这里声明表达式求值方法eval_expression,因为负数表达式求值要用到,而eval_expression方法的定义在负数表达式求值方法之后。至于为什么不把eval_expression的定义放在负数表达式之前,是因为eval_expression里面也要调用负数表达式求值方法,它们互相调用了对方,所以不管定义顺序如何,都需要在其中一个方法前声明另一个方法。

添加负数表达式求值方法,先对目标表达式求值,结果直接取负数即可。

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

static NL_Value eval_expression(Expression *exp);

static NL_Value
eval_minus_expression(Expression *exp) {
    NL_Value result = eval_expression(exp->u.minus_expression);
    if (result.type == INT_VALUE) {
        result.u.int_value = -result.u.int_value;
    } else if (result.type == DOUBLE_VAULE) {
        result.u.double_value = -result.u.double_value;
    } else {
        printf("[runtime error] eval minus expression with unexpected value type: %d.\n", result.type);
        exit(1);
    }
    return result;
}

表达式求值方法eval_expression也需要修改,switch中添加VARIABLE_EXPRESSIONMINUS_EXPRESSION两个case,分别调用变量表达式求值方法和负数表达式求值方法。其它二元表达式的case也添加了二元表达式求值的方法调用。

修改后的代码如下:

static NL_Value
eval_expression(Expression *exp) {
    NL_Value v;
    switch (exp->type) {
        case INT_EXPRESSION: {
            v = eval_int_expression(exp->u.int_value);
            break;
        }
        case DOUBLE_EXPRESSION: {
            v = eval_double_expression(exp->u.double_value);
            break;
        }
        case VARIABLE_EXPRESSION: {
            v = eval_variable_expression(exp);
            break;
        }
        case MINUS_EXPRESSION: {
            v = eval_minus_expression(exp);
            break;
        }
        case ADD_EXPRESSION:
        case SUB_EXPRESSION:
        case MUL_EXPRESSION:
        case DIV_EXPRESSION:
        case MOD_EXPRESSION: {
            v = nl_eval_binary_expression(exp->type, exp->u.binary_expression.left, exp->u.binary_expression.right);
            break;
        }
        case EXPRESSION_TYPE_PLUS:
        default: {
            printf("[runtime error] eval expression with unexpected type:%d\n", exp->type);
            exit(1);
        }
    }
    return v;
}

另外,原有的eval_binary_inteval_binary_double两个方法,switch中要补齐新的表达式类型case,补到default前的类型中。

增加更多执行方法

修改execute.c文件,增加更多语句执行方法,修改后的完整文件在这里

先给文件添加一个依赖

#include <stdlib.h>

添加表达式语句执行方法,其实就是直接调用表达式求值方法

void
nl_execute_expression_statement(Statement *statement) {
    nl_eval_expression(statement->u.expression);
}

修改赋值语句执行方法nl_execute_assign_statement,逻辑本质上没有变化,但语句结构体变化,相关逻辑也要变化,本质上先从语句拿到要赋值的变量名,还有值来源的表达式,先做表达式求值,从king中搜索记录的变量。如果搜索不到,则给king添加全局新的变量,如果能搜索到,则直接改变变量的值。

void
nl_execute_assign_statement(Statement *statement) {
    NL_Value val;
    Variable *var;
    King *king = nl_get_current_king();

    Expression *exp = statement->u.assign.expression;
    char *identifier = statement->u.assign.identifier;

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

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

添加打印语句执行方法,从语句结构体中拿到要打印的表达式,求值,打印值。

void
nl_execute_print_statement(Statement *statement) {
    NL_Value v = nl_eval_expression(statement->u.expression);
    nl_print_value(&v);
}

添加语句执行方法,switch判断语句类型,根据不同类型调用上面定义好的对应语句的执行方法。

void
nl_execute_statement(Statement *statement) {
    switch(statement->type) {
        case EXPRESSION_STATEMENT: {
            nl_execute_expression_statement(statement);
            break;
        }
        case ASSIGN_STATEMENT: {
            nl_execute_assign_statement(statement);
            break;
        }
        case PRINT_STATEMENT: {
            nl_execute_print_statement(statement);
            break;
        }
        case STATEMENT_TYPE_PLUS:
        default: {
            printf("[runtime error] in execute statement with unexpected type [%d].\n", statement->type);
            exit(1);
        }
    }
}

添加语句列表执行方法,就是遍历列表中每个语句,逐个调用语句执行方法去执行。

void
nl_execute_statement_list(StatementList *list) {
    StatementList *now;
    if (list == NULL) {
        return;
    }
    for (now = list; now; now = now->next) {
        nl_execute_statement(now->statement);
    }
}

修改语法逻辑

修改nl.y修改后的完整文件内容在这里

增加两种值类型,语句(statement)和语句列表(statement_list),增加语句类型的非终结变量和语句列表类型的非终结变量。

%type中定义的都是非终结符,非终结符即需要产生式进一步表达具体内容的变量。

%union {
    Expression *expression;
    char *identifier;
    Statement *statement;
    StatementList *statement_list;
}

%type <statement> statement expression_statement assign_statement print_statement
%type <statement_list> statement_list

之前语句列表的产生式,识别到语句和语句列表后并没有做任何逻辑,现在要加上相关逻辑。识别到语句需要创建语句添加到语句列表中,记录在king中。

statement_list
    : statement
    {
        King *king = nl_get_current_king();
        king->statement_list = nl_add_to_statement_list(king->statement_list, $1);
        $$ = king->statement_list;
    }
    | statement_list statement
    {
        King *king = nl_get_current_king();
        king->statement_list = nl_add_to_statement_list($1, $2);
        $$ = king->statement_list;
    }
    ;

语句的产生式分成三种细分语句产生式,表达式语句,赋值语句,打印语句。当然也需要给出各个语句的产生式。之前表达式语句是直接计算值,现在改成创建表达式语句。

statement
    : expression_statement
    | assign_statement
    | print_statement
    ;
expression_statement
    : expression SEMICOLON
    {
        $$ = nl_create_expression_statement($1);
    }
    ;
assign_statement
    : IDENTIFIER ASSIGN expression SEMICOLON
    {
        $$ = nl_create_assign_statement($1, $3);
    }
    ;
print_statement
    : PRINT LP expression RP SEMICOLON
    {
        $$ = nl_create_print_statement($3);
    }
    ;

对外接口增改

修改interface.c文件,修改后的完整文件内容在这里

其中NL_create_king方法中,创建king之后要多加一句语句列表初始化,语句列表是这次修改给king新加的属性。

king->statement_list = NULL;

添加执行开始的方法,本质上就是调用语句列表执行方法执行解析完成后的king中记录的语句列表。

void
NL_run(King *king) {
    nl_execute_statement_list(king->statement_list);
}

这个NL_run方法也要添加到对外类型定义的文件_NL.h中。

修改程序主入口方法

修改main.c文件,修改后的完整文件内容在这里

之前直接调用NL_compile(king, fp);就完事了,现在要在它之后多调用执行开始方法。

NL_compile(king, fp);
NL_run(king);

总结

本次修改,先修改原来的表达式创建方法,逻辑上是真正的创建,不包含执行。添加之前没有的表达式类型创建方法,添加各种语句创建方法,还完善了各种表达式计算方法和各种语句执行方法。在主入口main方法中,编译和执行也分开成两步。有了这个基础之后,下一篇将引入方法的使用,包括定义/声明方法以及调用方法。