编译原理入门课:(四)用词法解析处理多位数字和空白符

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

以前为了快速进入主题,咱们约定了表达式里只会出现个位数的数字。如今是时候打破这个规则,支持多位数的数字了。为了支持这点,咱们就须要接触一个新的步骤——词法分析。git

词法分析的做用

词法分析就是把一个完整的语句拆分红一个个词(token),方便以后进行进一步的语法分析。github

举个简单的例子:今天真热,将会被拆分红<今天>, <真>, <热>。固然拆分红<今>, <天真>, <热>也是一种可能,可是这样的分词方式不符合汉语的语法。正则表达式

好在计算机语言大部分是英文的,词与词之间通常用空白符隔开,很容易拆分。举个代码的例子:a = 1.1 + 2将被拆分为<id: a>, <等号>, <浮点数: 1.1>, <加号>, <整数: 2>,固然咱们也能够把加号和等号都算做运算符,作必定的聚合获得<id: a>, <运算符: =>, <浮点数: 1.1>, <运算符: +>, <整数: 2>json

有人要问为何拆分token的逻辑要作成单独的词法分析步骤,而不是放在语法分析里一块儿作?这是个好问题,还真的有点难回答。从我我的的观点来讲主要的两点多是:函数

  1. 词法分析不太适于用递归的方式来解析,性能会比较低,也不方便为不一样种类的token写特有的解析逻辑
  2. 词法分析做为单独的步骤,能够更方便独立的为token附加各类属性,为后续的步骤作准备

要问得更具体的话,仍是建议各位在本身写编译器的过程当中自行体会一下……😂性能

上面也提到了词法分析器主要是用来拆分token的,可是词法分析器还要负责一些别的工做。咱们总结下词法分析器的主要工做范围:ui

  1. 拆分token。
  2. 过滤掉多余的空白符,发现没法识别的无效字符并报错。
  3. 记录代码中每一个token的位置信息,方便在编译出错时能够定位到具体的位置。
  4. 宏定义处理。
  5. 和符号表进行交互。例如定义函数时把函数名加入函数表,方便重复定义同名函数时进行报错。

我准备一开始作的简单点,先把必备的前两条功能给实现了。spa

词法分析的实现

定义要用到的结构体和枚举

首先咱们得定义一个用来描述token的结构体:3d

typedef struct {
    int type;
    int value;
} slm_token;
复制代码

关于token的类型,前文也提到了,运算符能够作必定聚合,也能够每种运算符算一种类型。我这里就不作聚合了,把咱们前文出现过的token类型都定义出来:

enum {
    SLM_EXPRESSION_TOKEN_UNKNOWN = 0,
    SLM_EXPRESSION_TOKEN_DIGITS,
    SLM_EXPRESSION_TOKEN_ADD,
    ...
    SLM_EXPRESSION_TOKEN_CLOSE,
    SLM_EXPRESSION_TOKEN_END
};
复制代码

而后扩展下咱们的slm_expr结构体,之后语法分析器就不该该直接读expStr而应该从token里取值啦:

typedef struct {
    const char *expStr;
    slm_token token;
    int errType;
} slm_expr;
复制代码

词法分析核心实现及状态机

词法分析的核心函数通常叫作next或者scan,咱们这里就叫它next吧。它主要实现的功能是读取下一个有效的token,存到slm_expr结构体的token成员里以供语法分析器使用。

C语言的词法分析十分简单,由于根据token的首字符就能区分出token的类型:若是首字符是数字那必定是个数值token;若是首字符是字母或下划线那必定是个id类的token,至于这个id是关键字仍是函数名、变量名那就另说了。怎么样?是否是忽然明白了大部分计算机语言里变量名不能以数字开头的缘由?

这一章里面咱们暂时还用不到id类的token,因此主要讲一下数值token的处理:

void next(slm_expr *e) {
    ...
    if (isdigit(*e->expStr)) {
        e->token.type = SLM_EXPRESSION_TOKEN_DIGITS;
        e->token.value = *e->expStr - '0';
        (e->expStr)++;
        while (isdigit(*e->expStr)) {
            e->token.value = e->token.value * 10 + (*e->expStr - '0');
            (e->expStr)++;
        }
    }
    ...
}
复制代码

可见若是发现一个token是以数字开头的,那么咱们能够循环读取后面连续的数字,直接把整个token完整的数字值读取出来,供语法分析器在后面的分析中使用。

词法分析的过程通常能够用状态机来描述,上面的解析过程对应的状态机能够用这么个图来表示:

09-A

能够看出来这种图和流程图类似,更适合用条件分支及循环语句来实现它的逻辑。而后咱们能够大体的补全一下整个词法分析器的状态机图:

09-B

接下来照着状态机图来实现代码逻辑就行了,在这里不贴完整代码了。注意若是出现了用状态机没法描述的token,那么这必定是个非法的token。

用状态机图能够直观的表示词法分析的流程,之后扩展数值类型支持浮点数之类的,均可以从画状态机图开始。好比大部分计算机语言支持的数字类型,能够用下面的状态机图来表示(图片来自Online JSON Viewer):

09-C

除了状态机,另外一个超级适于描述词法分析器的就是正则表达式,有兴趣的同窗能够自行去了解下。著名的词法分析器Lex就是用正则表达式描述词法规则的。

给语法分析器接入词法分析器

以最典型的number函数为例:

int number(slm_expr *e) {
    int hasMinus = 0;
    if (e->token.type == SLM_EXPRESSION_TOKEN_SUB_OR_MINUS) {
        TRY(next(e));
        hasMinus = 1;
    }
    if (e->token.type != SLM_EXPRESSION_TOKEN_DIGITS) {
        THROW(SLM_EXPRESSION_ERROR_EXPECT_DIGIT);
    }
    int result = e->token.value;
    TRY(next(e));
    if (hasMinus) {
        result *= -1;
    }
    return result;
}
复制代码

咱们把以前从e->expStr直接取值的代码都换成读取e->token。还要把(e->expStr)++的地方都替换为TRY(next(e)),加上TRY是由于next里面也会报非法token的错误。固然不能忘记的是,在main函数里必须预先调用一次next,否则首次进入语法解析器的时候e->token会是空的。

把全部语法分析步骤里的代码替换完以后,咱们就能够获得一个能剔除空格、识别非法字符和多位数字的解析器啦。完整的代码我就不在这里全贴出来了,存放在SlimeExpressionC,欢迎你们自取。

相关文章
相关标签/搜索