编译器实现之旅——第三章 实现词法分析器前的准备

在这一章的旅程中,咱们将要为整个编译器的“前端中的前端”:词法分析器的实现作好充足的准备。前端

1. 词法分析器概观

纵观编译器的输入:源代码,咱们不难发现,源代码说白了也就是一个很长很长的字符串。而说到字符串,咱们不难想到字符串的分割函数。这类分割函数以空格,或任意的什么字符或字符串做为分隔符,将一个字符串分割成多个小字符串片断。这不就是词法分析器么?你可能会想。可是,咱们将遇到这样的问题:算法

"1 + 1" -> ("1", "+", "1")
    "1+1"   -> ?

确实,使用普通的字符串分割函数能够很轻易的将上面第一个字符串进行分割,但咱们发现,不管怎么设置分隔符,咱们都没法将第二个字符串也分割成一样的结果了。也就是说,普通的字符串分割函数及其算法是不能胜任词法分析器的工做的,咱们必须另想办法。函数

要想分割一个字符串,其思路无非就是寻找一个分割点,而后将当前起点到分割点的这段字符串分割出去,再将当前起点设置于分割点以后,并继续寻找下一个分割点,不断重复这个过程,直至到达字符串的结尾。那么,为何字符串分割函数不能胜任词法分析器的工做呢?略加思索不难发现缘由:字符串分割函数的“寻找下一个分割点”的逻辑过于简单了,只是一个相等性判断。而咱们所须要的逻辑更复杂,好比:看到一个空格,就分割;再好比:看到一个不是数字的字符,就分割;等等。因此,只要咱们扩充字符串分割函数的“寻找下一个分割点”的逻辑,咱们就能实现出词法分析器了。code

2. 词法分析器的状态

咱们首先须要作什么呢?咱们须要为词法分析器定义许多不一样的状态,处于不一样状态的词法分析器执行不一样的行为。显然,词法分析器须要一个开始状态,一个完成状态,其可能还须要一个或多个中间状态。词法分析器从开始状态开始,不断读取源代码中的每一个字符,最终结束于完成状态,当词法分析器处于完成状态时,其就分割出了一个记号。词法分析器不断执行这样的“开始, ..., 完成”过程,直至到达字符串的结尾。token

为了获知词法分析器到底须要哪些状态,咱们须要看一看CMM语言对于记号的定义。请注意,这里的记号是广义的,其不只表明一个英文单词,还表明一个符号,一串数字等,即,一个记号就是词法分析器须要分割出来的一段字符串。CMM语言对于记号的定义以下所示:字符串

  1. 一串连续的,由大写或小写字母构成的字符串
  2. 一串连续的,由数字构成的字符串
  3. 这些符号:+ - * / < <= > >= == != = ; , ( ) [ ] { }
  4. /* ... */ 构成注释
  5. 关键词:void int if else while return

这里须要说明的是:所谓关键词,仅仅是上述第1条的一种特例。即:当咱们分割出一个单词时,咱们须要额外断定一下这个单词是否是关键词,若是是,则咱们须要将这个单词的类别从“单词”变为“关键词XX”。例如:当咱们分割出字符串“abc”时,咱们将其归类为“单词”;而当咱们分割出字符串“if”时,咱们就须要将其归类为“关键词if”。编译器

有了CMM语言对于记号的定义,咱们就能够着手考虑词法分析器到底须要哪些状态了。咱们不妨以上述第一条规则为例进行思考,即:为了分割出一个单词,词法分析器须要哪些状态?string

首先,词法分析器从“开始”状态开始,若是此时词法分析器读入了一个大写或小写字母,则咱们知道:接下来读取到的将是一个单词了;但同时,仅凭读取到的这个字符,咱们永远不可能知道当前的这个单词是否已经读取结束;咱们只有看到一个不是大写或小写字母的字符时,才能肯定刚刚的这个单词已经读取结束了,咱们应令词法分析器进入“完成”状态。为了处理这种状况,咱们引入中间状态“正在读取单词”。当词法分析器读入一个大写或小写字母时,其应当即由“开始”状态转入“正在读取单词”状态;词法分析器应保持这个状态,并不断读入新的字符,直至当前读入的字符不是大写或小写字母,此时,词法分析器应当即由“正在读取单词”状态转入“完成”状态,完成这次分割。it

那么,如何利用上述思路,使词法分析器跳过注释呢?请看:编译

首先,词法分析器仍是从“开始”状态开始,当其读入一个“/”时,咱们此时并不知道这个“/”是一个除号,仍是注释的开始,故咱们先令词法分析器进入“正在读取除号”这个中间状态。在此状态中,若是词法分析器读入的下一个字符是一个“”,则此时咱们就能够肯定词法分析器如今进入了注释中,咱们就再令词法分析器转入“正在读取注释”状态;反之,若是词法分析器读入的下一个字符不是一个“”,咱们也能够肯定词法分析器此次读取到的真的是一个除号,此时,咱们固然是令词法分析器进入“完成”状态。

当词法分析器处于“正在读取注释”状态中时,咱们须要关注两件事:

  1. 词法分析器应丢掉任何读取到的字符
  2. 词法分析器应努力的逃离注释

怎么逃离注释呢?显然,若是要逃离注释,咱们就须要同时知足这两个条件:

  1. 遇到一个“*”
  2. 紧接着,再遇到一个“/”

因此,当词法分析器被困在注释中时,其一边一视同仁的丢掉一切读取到的字符,一边也留心着读取到的字符是否是“”,若是是,词法分析器就看到了但愿。此时,词法分析器应转入“正在逃离注释”状态,在这个状态下,若是词法分析器又读取到了“/”,那么恭喜,此时词法分析器就成功的逃离了注释,又回到了久违的“开始”状态;若是不是“/”,但愿也没有彻底破灭,此时,若是词法分析器读取到的仍是“”,那么其就还应该停留在“正在逃离注释”状态;而若是读取到的既不是“/”也不是“*”,那么很遗憾,逃离就完全失败了,词法分析器又将回退到“正在读取注释”状态。

利用上述思路触类旁通,咱们便可获得词法分析器所须要的全部状态了。请看:

  1. 显然,咱们须要“开始”和“完成”状态
  2. 为了读取单词和数字,咱们须要“正在读取单词”和“正在读取数字”状态
  3. 为了处理注释相关问题,咱们须要“正在读取除号”、“正在读取注释”和“正在逃离注释”状态
  4. 为了明确词法分析器读取到的究竟是“<”仍是“<=”、是“>”仍是“>=”、是“=”仍是“==”,咱们须要“正在读取小于号”、“正在读取大于号”和“正在读取等号”状态
  5. 为了使词法分析器正确的读取到“!=”(而不是“!” + 别的错误符号),咱们须要“正在读取不等号”状态

至此,咱们就获得了词法分析器所须要的全部状态。代码以下所示:

enum class LEXER_STAGE
{
    // Start
    START,

    // abc...
    //  ^^^^^
    IN_ID,

    // 123...
    //  ^^^^^
    IN_NUMBER,

    // /?
    //  ^
    IN_DIVIDE,

    // /* ...
    //    ^^^
    IN_COMMENT,

    // ... */
    //      ^
    END_COMMENT,

    // <?
    //  ^
    IN_LESS,

    // >?
    //  ^
    IN_GREATER,

    // =?
    //  ^
    IN_ASSIGN,

    // !?
    //  ^
    IN_NOT,

    // Done
    DONE,
};

3. 记号的类别

当词法分析器读取到一个记号后,咱们就须要将其进行归类。有了词法分析器的各类状态的辅助,这样的归类将变的十分容易。例如,当咱们从“正在读取数字”状态转移至“完成”状态时,咱们固然知道当前的这个记号的类别是“数字”;而当咱们读取到一个“(”时,咱们固然也知道这个记号的类别是“左圆括号”;以此类推。咱们能够从上文中给出的记号的定义中,获得全部记号的类别。代码以下所示:

enum class TOKEN_TYPE
{
    // Word
    ID,                    // ID
    NUMBER,                // Number

    // Keyword
    VOID,                  // void
    INT,                   // int
    IF,                    // if
    ELSE,                  // else
    WHILE,                 // while
    RETURN,                // return

    // Operator
    PLUS,                  // +
    MINUS,                 // -
    MULTIPLY,              // *
    DIVIDE,                // /
    LESS,                  // <
    LESS_EQUAL,            // <=
    GREATER,               // >
    GREATER_EQUAL,         // >=
    EQUAL,                 // ==
    NOT_EQUAL,             // !=
    ASSIGN,                // =
    SEMICOLON,             // ;
    COMMA,                 // ,
    LEFT_ROUND_BRACKET,    // (
    RIGHT_ROUND_BRACKET,   // )
    LEFT_SQUARE_BRACKET,   // [
    RIGHT_SQUARE_BRACKET,  // ]
    LEFT_CURLY_BRACKET,    // {
    RIGHT_CURLY_BRACKET,   // }

    // EOF
    END_OF_FILE,           // EOF

    // AST
    DECL_LIST,             // AST: DeclList
    VAR_DECL,              // AST: VarDecl
    FUNC_DECL,             // AST: FuncDecl
    PARAM_LIST,            // AST: ParamList
    PARAM,                 // AST: Param
    COMPOUND_STMT,         // AST: CompoundStmt
    LOCAL_DECL,            // AST: LocalDecl
    STMT_LIST,             // AST: StmtList
    IF_STMT,               // AST: IfStmt
    WHILE_STMT,            // AST: WhileStmt
    RETURN_STMT,           // AST: ReturnStmt
    EXPR,                  // AST: Expr
    VAR,                   // AST: Var
    SIMPLE_EXPR,           // AST: SimpleExpr
    ADD_EXPR,              // AST: AddExpr
    TERM,                  // AST: Term
    CALL,                  // AST: Call
    ARG_LIST,              // AST: ArgList
};

须要说明的是,上述代码的最后一部分是AST节点类别,与词法分析器无关。咱们将在后续的旅程中讲述这部分类别的做用。

4. 其余准备工做

在实现词法分析器以前,咱们还有一些比较简单的准备工做须要作,列举以下:

  1. 咱们须要定义一个用于保存记号的结构体:
struct Token
{
    // Attribute
    TOKEN_TYPE tokenType;
    string tokenStr;
    int lineNo;
};

在这个结构体中,咱们保存了记号的类别、记号字符串,以及这个记号在源代码中所处的行数。

  1. 咱们须要定义一个哈希表,以完成普通单词到关键词的识别与转换:
const unordered_map<string, TOKEN_TYPE> KEYWORD_MAP
{
    {"void",   TOKEN_TYPE::VOID},
    {"int",    TOKEN_TYPE::INT},
    {"if",     TOKEN_TYPE::IF},
    {"else",   TOKEN_TYPE::ELSE},
    {"while",  TOKEN_TYPE::WHILE},
    {"return", TOKEN_TYPE::RETURN},
};

经过键的存在性检测,咱们就能够断定一个单词是不是一个关键词了;若是是,咱们也能够获得这个关键词所对应的记号的类别。

  1. 咱们须要定义一个报错函数,用于在词法分析器发现语法错误时报错并退出:
void InvalidChar(char invalidChar, int lineNo)
{
    printf("Invalid char: %c in line: %d\n", invalidChar, lineNo);

    exit(1);
}

至此,咱们就完成了全部准备工做,能够开始实现词法分析器了。请看下一章:《实现词法分析器》。

相关文章
相关标签/搜索