编译原理入门课:(五)解析ID型词法和函数调用语法

原文地址:苹果梨的博客html

上一章词法分析的内容里咱们介绍了解析数字的方法,当时还提到了对ID的解析,可是由于当时还用不到ID类型,因此就没有作对应的解析,这一章咱们将会讲解下ID类型的解析方法。前端

ID类型一般用在变量名和函数名上,变量要应用的话至少还得实现赋值表达式,因此咱们先用ID类型来尝试实现函数调用功能。注意是调用咱们在计算器里内置的函数,暂时尚未办法动态定义新的函数。git

ID类型的词法分析

有了上一章的基础,ID类型的解析应该对你们是易如反掌了,简单到我都懒得画状态机图了:github

typedef struct {
    int type;
    int value;
    char* name;
} slm_token;

void next(slm_expr *e) {
    ...
    if (isalpha(*e->expStr)) {
        e->token.type = SLM_EXPRESSION_TOKEN_ID;
        const char *start = e->expStr;
        do {
            (e->expStr)++;
        } while (isalpha(*e->expStr) || isdigit(*e->expStr));
        size_t length = e->expStr - start;
        char *name = calloc(length + 1, sizeof(char));
        strncpy(name, start, length);
        name[length] = '\0';
        e->token.name = name;
    }
    ...
}
复制代码

咱们在slm_token结构体里新增了一个name字段用来存储解析出的ID,这里的ID型token以字母开头,后面能够跟着多位字母或数字。C语言里的内存管理是要重点注意的,必定要在适当的时机释放掉本身申请的内存,我一开始也漏了一处😂。数组

函数调用的语法分析

哈哈,又回到语法分析步骤了。回到语法分析,首先要写的就是文法,此次只列出来要修改的文法:bash

factor -> number | func | '(' expr ')'
func   -> func1 | func2
func1  -> id '(' expr ')'
func2  -> id '(' expr ',' expr ')'
复制代码

咱们只打算支持三个函数:max(x, y)min(x, y)abs(x)。由于这里只有单参数和双参数两种状况,因此文法就直接生硬的把全部状况列出来了。有了给定的文法,想必写逻辑也不是难事。由于懒得去弄一个新的变量暂存函数名字串,因此我直接把三个函数展开成三个if段来写:函数

int func(slm_expr *e) {
    if (e->token.type != SLM_EXPRESSION_TOKEN_ID || !e->token.name) {
        THROW(SLM_EXPRESSION_ERROR_EXPECT_ID);
    }
    int result;
    if (strcmp(e->token.name, "max") == 0) {
        TRY(next(e));
        // '('
        if (e->token.type != SLM_EXPRESSION_TOKEN_OPEN) {
            THROW(SLM_EXPRESSION_ERROR_EXPECT_OPEN_PARENTHESIS);
        }
        TRY(next(e));
        // arg1
        int arg1 = TRY(expr(e));
        // ','
        if (e->token.type != SLM_EXPRESSION_TOKEN_COMMA) {
            THROW(SLM_EXPRESSION_ERROR_EXPECT_COMMA);
        }
        TRY(next(e));
        // arg2
        int arg2 = TRY(expr(e));
        // ')'
        if (e->token.type != SLM_EXPRESSION_TOKEN_CLOSE) {
            THROW(SLM_EXPRESSION_ERROR_EXPECT_CLOSE_PARENTHESIS);
        }
        TRY(next(e));
        // result
        result = arg1 >= arg2 ? arg1 : arg2;
    } else if (strcmp(e->token.name, "min") == 0) {
        TRY(next(e));
        // '('
        if (e->token.type != SLM_EXPRESSION_TOKEN_OPEN) {
            THROW(SLM_EXPRESSION_ERROR_EXPECT_OPEN_PARENTHESIS);
        }
        TRY(next(e));
        // arg1
        int arg1 = TRY(expr(e));
        // ','
        if (e->token.type != SLM_EXPRESSION_TOKEN_COMMA) {
            THROW(SLM_EXPRESSION_ERROR_EXPECT_COMMA);
        }
        TRY(next(e));
        // arg2
        int arg2 = TRY(expr(e));
        // ')'
        if (e->token.type != SLM_EXPRESSION_TOKEN_CLOSE) {
            THROW(SLM_EXPRESSION_ERROR_EXPECT_CLOSE_PARENTHESIS);
        }
        TRY(next(e));
        // result
        result = arg1 <= arg2 ? arg1 : arg2;
    } else if (strcmp(e->token.name, "abs") == 0) {
        TRY(next(e));
        // '('
        if (e->token.type != SLM_EXPRESSION_TOKEN_OPEN) {
            THROW(SLM_EXPRESSION_ERROR_EXPECT_OPEN_PARENTHESIS);
        }
        TRY(next(e));
        // arg1
        result = TRY(expr(e));
        // ')'
        if (e->token.type != SLM_EXPRESSION_TOKEN_CLOSE) {
            THROW(SLM_EXPRESSION_ERROR_EXPECT_CLOSE_PARENTHESIS);
        }
        TRY(next(e));
        // result
        result = abs(result);
    } else {
        THROW(SLM_EXPRESSION_ERROR_UNKNOW_FUNCTION);
    }
    return result;
}
复制代码

作一些测试验证下逻辑是否正常,函数调用功能就算完成啦。到这一步的完整代码能够在SlimeExpressionC-chapter5.1得到。学习

用函数表优化函数解析

不用我说,你们应该也以为上一节的函数解析逻辑写得太丑了,这种代码不该该存在于咱们的库里!测试

首先咱们确定不能给每个函数写一个if段,那么咱们确定须要一个表来存储咱们支持的全部函数,这样就能够在表里查询咱们支持的函数该怎么处理了。而后就是要考虑函数怎么存在内存里呢?答案固然是用函数指针呀。咱们先作的粗糙一点,把参数数量不一样的函数定义成不一样的结构体成员:优化

typedef struct {
    const char *name;
    int argCount;
    int (*func1)(int);
    int (*func2)(int, int);
} slm_func;

const int FuncCount = 3;
const int ArgMaxCount = 2;
const slm_func FuncList[FuncCount] = {
    {.name = "max", .argCount = 2, .func2 = &slm_max},
    {.name = "min", .argCount = 2, .func2 = &slm_min},
    {.name = "abs", .argCount = 1, .func1 = &slm_abs},
};
复制代码

这样,咱们的函数表就先搞定了。把函数指针对应的函数给实现一下:

int slm_abs(int x) {
    return x < 0 ? -x : x;
}

int slm_max(int x, int y) {
    return x >= y ? x : y;
}

int slm_min(int x, int y) {
    return x <= y ? x : y;
}
复制代码

接下来的重头戏就是对func函数的改造啦,有了思路你们应该也大概能知道实现是什么样的了:

int func(slm_expr *e) {
    if (e->token.type != SLM_EXPRESSION_TOKEN_ID || !e->token.name) {
        THROW(SLM_EXPRESSION_ERROR_EXPECT_ID);
    }
    for (int i = 0; i < FuncCount; i++) {
        slm_func funcItem = FuncList[i];
        if (strcmp(e->token.name, funcItem.name) == 0) {
            TRY(next(e));
            // '('
            if (e->token.type != SLM_EXPRESSION_TOKEN_OPEN) {
                THROW(SLM_EXPRESSION_ERROR_EXPECT_OPEN_PARENTHESIS);
            }
            TRY(next(e));
            // arg1
            int args[ArgMaxCount];
            args[0] = TRY(expr(e));
            // arg2 ~ argN
            for (int j = 1; j < funcItem.argCount; j++) {
                // ','
                if (e->token.type != SLM_EXPRESSION_TOKEN_COMMA) {
                    THROW(SLM_EXPRESSION_ERROR_EXPECT_COMMA);
                }
                TRY(next(e));
                // arg2 ~ argN
                args[j] = TRY(expr(e));
            }
            // ')'
            if (e->token.type != SLM_EXPRESSION_TOKEN_CLOSE) {
                THROW(SLM_EXPRESSION_ERROR_EXPECT_CLOSE_PARENTHESIS);
            }
            TRY(next(e));
            // result
            switch (funcItem.argCount) {
                case 1:
                    return (*funcItem.func1)(args[0]);
                    break;
                case 2:
                    return (*funcItem.func2)(args[0], args[1]);
                    break;
            }
            break;
        }
    }
    THROW(SLM_EXPRESSION_ERROR_UNKNOW_FUNCTION);
}
复制代码

固然这代码仍是有优化空间的,好比咱们能够用hash表来存储函数表,加快检索的效率;好比咱们能够用链表或者数组之类的手段传递参数,这样就能够动态的支持无限多的参数。不过这些就不深刻在这里展开啦,目前完成的完整的代码放在SlimeExpressionC-chapter5.2

借助函数表也是之后咱们实现动态定义新函数的关键点,到时候你们把FuncList定义成可变的就好,具体深刻的作法这里我也不展开了,由于那个已经超出入门课的范畴啦!

入门课总结

到这里咱们的编译原理入门课就告一段落了。经过实现一些简单的表达式解析计算功能,咱们把编译器前端的语法分析和词法分析工做原理讲了个大概。过程当中也介绍了一些设计编译器的方法,你们掌握了以后应该也能够在其基础上,作出一些本身想要的功能,例如实现变量和赋值表达式等。我也只能作到领你们入个门,更深刻的知识就须要你们本身再去找资料深刻学习啦(前言里我也列了一些资料)。

那么但愿个人入门课对你有所帮助。若是之后个人懒癌痊愈了,兴许会再写个编译原理中级课吧~再见~😁

相关文章
相关标签/搜索