编译器实现之旅——第四章 实现词法分析器

在上一章的旅程中,我们讨论了词法分析器的实现思路,我们也为词法分析器的实现做了许多准备工作。现在,就让我们来实现词法分析器吧。

1. 词法分析器的类定义

词法分析器的类定义如下:

class __LexicalAnalyzer
{
    // Friend
    friend class Core;


public:

    // Constructor
    explicit __LexicalAnalyzer(const string &inputFilePath = "");


private:

    // Attribute
    string __inputFilePath;


    // Invalid Char
    static void __invalidChar(char curChar, int lineNo);


    // Next __Token __LexerStage::__Start Stage
    static void __nextTokenStartStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
        string &tokenStr, int &lineNo);


    // Next __Token __LexerStage::__InId Stage
    static void __nextTokenInIDStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
        string &tokenStr, int &);


    // Next __Token __LexerStage::__InNumber Stage
    static void __nextTokenInNumberStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
        string &tokenStr, int &);


    // Next __Token __LexerStage::__InDivide Stage
    static void __nextTokenInDivideStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
        string &tokenStr, int &);


    // Next __Token __LexerStage::__InComment Stage
    static void __nextTokenInCommentStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &,
        string &, int &);


    // Next __Token __LexerStage::__EndComment Stage
    static void __nextTokenEndCommentStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &,
        string &, int &);


    // Next __Token __LexerStage::__InLess Stage
    static void __nextTokenInLessStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
        string &tokenStr, int &);


    // Next __Token __LexerStage::__InGreater Stage
    static void __nextTokenInGreaterStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
        string &tokenStr, int &);


    // Next __Token __LexerStage::__InAssign Stage
    static void __nextTokenInAssignStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
        string &tokenStr, int &);


    // Next __Token __LexerStage::__InNot Stage
    static void __nextTokenInNotStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
        string &tokenStr, int &lineNo);


    // Next __Token
    static __Token __nextToken(const char *&codePtr, int &lineNo);


    // Lexical Analysis
    vector<__Token> __lexicalAnalysis() const;
};

可见,词法分析器的核心函数是__nextToken,每次调用这个函数,词法分析器都会返回下一个解析到的Token;为了实现词法分析器的各个状态的不同行为,我们定义了多个对应于各个状态的分派函数。

2. 各个周边函数的实现

接下来,我们来看看词法分析器的各个周边函数的实现:

__LexicalAnalyzer::__LexicalAnalyzer(const string &inputFilePath):
    __inputFilePath(inputFilePath) {}


void __LexicalAnalyzer::__invalidChar(char curChar, int lineNo)
{
    throw runtime_error((format("Invalid char: %c in line: %d") %
        curChar                                                 %
        lineNo
    ).str());
}


vector<__Token> __LexicalAnalyzer::__lexicalAnalysis() const
{
    vector<__Token> tokenList;

    ifstream f(__inputFilePath);
    string codeStr;
    int lineNo = 1;

    getline(f, codeStr, '\0');

    auto codePtr = codeStr.c_str();

    for (auto tokenObj = __nextToken(codePtr, lineNo); /* See below */; tokenObj = __nextToken(codePtr, lineNo))
    {
        tokenList.push_back(tokenObj);

        if (tokenObj.__tokenType == __TokenType::__END)
        {
            break;
        }
    }

    return tokenList;
}

__invalidChar函数是一个报错函数,其用于在词法分析器发现语法错误时报错并退出;__lexicalAnalysis函数通过不断调用__nextToken函数,收集代码中全部的Token。

其他周边函数的实现都很简单,这里就不讨论了。

3. __nextToken函数的实现

接下来,我们来看看词法分析器中最重要的__nextToken函数的实现:

__Token __LexicalAnalyzer::__nextToken(const char *&codePtr, int &lineNo)
{
    __LexerStage lexerStage = __LexerStage::__Start;
    __TokenType tokenType;
    string tokenStr;

    while (lexerStage != __LexerStage::__Done)
    {
        switch (lexerStage)
        {
            case __LexerStage::__Start:
                __nextTokenStartStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__InId:
                __nextTokenInIDStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__InNumber:
                __nextTokenInNumberStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__InDivide:
                __nextTokenInDivideStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__InComment:
                __nextTokenInCommentStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__EndComment:
                __nextTokenEndCommentStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__InLess:
                __nextTokenInLessStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__InGreater:
                __nextTokenInGreaterStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__InAssign:
                __nextTokenInAssignStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            case __LexerStage::__InNot:
                __nextTokenInNotStage(codePtr, lexerStage, tokenType, tokenStr, lineNo);
                break;

            default:
                throw runtime_error("Invalid __LexerStage value");
        }
    }

    return __Token(tokenType, tokenStr, lineNo);
}

在解析开始前,我们首先将词法分析器的状态置为"开始"状态,然后不断循环,直至词法分析器的状态变为"完成"状态。在循环体中,词法分析器不断读入下一个字符,并可能将当前字符存入tokenStr中。接下来,根据词法分析器的不同状态,分别调用各个分派函数。这些分派函数均具有修改codePtr、lexerStage、tokenType、tokenStr、lineNo这些变量的能力。最终,当词法分析器的状态变为"完成"状态时,我们构造并返回一个Token。

接下来,我们来看看各个分派函数的实现,首先从"开始"状态的分派函数开始。

4. "开始"状态的分派函数的实现

__nextTokenStartStage函数用于在词法分析器处于"开始"状态时被调用,其实现如下:

void __LexicalAnalyzer::__nextTokenStartStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
    string &tokenStr, int &lineNo)
{
    if (isalpha(*codePtr))
    {
        lexerStage = __LexerStage::__InId;
        tokenStr += *codePtr++;
    }
    else if (isdigit(*codePtr))
    {
        lexerStage = __LexerStage::__InNumber;
        tokenStr += *codePtr++;
    }
    else if (isspace(*codePtr))
    {
        if (*codePtr == '\n')
        {
            lineNo++;
        }

        codePtr++;
    }
    else
    {
        switch (*codePtr)
        {
            case '+':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__Plus;
                tokenStr += *codePtr++;
                break;

            case '-':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__Minus;
                tokenStr += *codePtr++;
                break;

            case '*':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__Multiply;
                tokenStr += *codePtr++;
                break;

            case '/':
                lexerStage = __LexerStage::__InDivide;
                codePtr++;
                break;

            case '<':
                lexerStage = __LexerStage::__InLess;
                tokenStr += *codePtr++;
                break;

            case '>':
                lexerStage = __LexerStage::__InGreater;
                tokenStr += *codePtr++;
                break;

            case '=':
                lexerStage = __LexerStage::__InAssign;
                tokenStr += *codePtr++;
                break;

            case '!':
                lexerStage = __LexerStage::__InNot;
                tokenStr += *codePtr++;
                break;

            case ';':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__Semicolon;
                tokenStr += *codePtr++;
                break;

            case ',':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__Comma;
                tokenStr += *codePtr++;
                break;

            case '(':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__LeftRoundBracket;
                tokenStr += *codePtr++;
                break;

            case ')':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__RightRoundBracket;
                tokenStr += *codePtr++;
                break;

            case '[':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__LeftSquareBracket;
                tokenStr += *codePtr++;
                break;

            case ']':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__RightSquareBracket;
                tokenStr += *codePtr++;
                break;

            case '{':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__LeftCurlyBracket;
                tokenStr += *codePtr++;
                break;

            case '}':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__RightCurlyBracket;
                tokenStr += *codePtr++;
                break;

            case '\0':
                lexerStage = __LexerStage::__Done;
                tokenType = __TokenType::__END;
                break;

            default:
                __invalidChar(*codePtr, lineNo);
                break;
        }
    }
}

"开始"状态是整个词法分析器中最复杂的状态。在这个状态下,词法分析器可能会遇到并处理很多种情况,列举如下:

  1. 如果当前读取到的字符是一个字母,则词法分析器应进入"正在读取单词"状态
  2. 同理,如果当前读取到的字符是一个数字,则词法分析器应进入"正在读取数字"状态
  3. 如果当前读取到的字符是一个空白符,则词法分析器应停留在"开始"状态,并丢掉当前读取到的字符。特别的,如果当前读取到的字符是一个换行符,则当前行数需要加1
  4. 如果当前读取到的字符是"+"、"-"、"*"、";"、","、"("、")"、"["、"]"、"{"、"}"或EOF这些仅由一个字符构成的记号,则我们可以立即确定当前记号的类别,并令词法分析器立即进入"完成"状态
  5. 如果当前读取到的字符是"/"、"<"、">"、"="或"!",则词法分析器应进入各中间状态
  6. 如果当前读取到的字符不满足上述各种情况之一,则报错退出

这里需要额外说明的是,如果当前读取到的字符是一个"/",则我们此时并不知道这个"/"需不需要被保存下来。我们必须等到确定这个"/"是一个除号时,再保存这个"/"。

5. "正在读取单词/数字"状态的分派函数的实现

__nextTokenInIDStage与__nextTokenInNumberStage函数分别用于在词法分析器处于"正在读取单词"和"正在读取数字"状态时被调用,其实现如下:

void __LexicalAnalyzer::__nextTokenInIDStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
    string &tokenStr, int &)
{
    if (isalpha(*codePtr))
    {
        tokenStr += *codePtr++;
    }
    else
    {
        lexerStage = __LexerStage::__Done;
        tokenType  = __Constants::__KEYWORD_MAP.count(tokenStr) ? __Constants::__KEYWORD_MAP.at(tokenStr) : __TokenType::__Id;
    }
}


void __LexicalAnalyzer::__nextTokenInNumberStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
    string &tokenStr, int &)
{
    if (isdigit(*codePtr))
    {
        tokenStr += *codePtr++;
    }
    else
    {
        lexerStage = __LexerStage::__Done;
        tokenType  = __TokenType::__Number;
    }
}

当词法分析器处于"正在读取单词"状态时,如果当前读取到的字符还是一个字母,那么此时我们只需要将当前字符加入tokenStr即可,词法分析器的状态不变;如果不是,则我们知道:此时单词已经读完了,且很重要的一点是:当前读取到的字符并不算在这个单词内,所以,我们既不能将当前字符加入tokenStr,也不能推进codePtr,而只需要将词法分析器的状态置为"完成"状态即可;此外,我们需要查阅关键词表,以确定当前读取到的单词是否是一个关键词。

当词法分析器处于"正在读取数字"状态时,情况与"正在读取单词"状态是几乎一致的。唯独不同的是,读取数字时不需要进行关键词判定。

6. "正在读取除号"状态的分派函数的实现

__nextTokenInDivideStage函数用于在词法分析器处于"正在读取除号"状态时被调用,其实现如下:

void __LexicalAnalyzer::__nextTokenInDivideStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
    string &tokenStr, int &)
{
    if (*codePtr == '*')
    {
        lexerStage = __LexerStage::__InComment;
        codePtr++;
    }
    else
    {
        lexerStage = __LexerStage::__Done;
        tokenType  = __TokenType::__Divide;
        tokenStr   = "/";
    }
}

当词法分析器已经读取到了一个"/"后,其进入"正在读取除号"状态(请注意,此时的"/"还没有被存入tokenStr)。此时,如果又读取到了一个"*",则词法分析器应进入"正在读取注释"状态;反之,如果读取到的字符不是"*",我们就可以确定:之前读取到的"/"真的是一个除号。那么我们就需要将词法分析器的状态置为"完成"状态,并设定记号的类别和记号字符串(但不推进codePtr)。

7. "正在读取/离开注释"状态的分派函数的实现

__nextTokenInCommentStage和__nextTokenEndCommentStage函数分别用于在词法分析器处于"正在读取注释"和"正在离开注释"状态时被调用,其实现如下:

void __LexicalAnalyzer::__nextTokenInCommentStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &,
    string &, int &lineNo)
{
    if (*codePtr == '*')
    {
        lexerStage = __LexerStage::__EndComment;
    }
    else if (*codePtr == '\n')
    {
        lineNo++;
    }

    codePtr++;
}


void __LexicalAnalyzer::__nextTokenEndCommentStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &,
    string &, int &lineNo)
{
    if (*codePtr == '/')
    {
        lexerStage = __LexerStage::__Start;
    }
    else if (*codePtr != '*')
    {
        lexerStage = __LexerStage::__InComment;

        if (*codePtr == '\n')
        {
            lineNo++;
        }
    }

    codePtr++;
}

首先,不管是哪个函数,只要和注释搭边了,我们都不保存当前字符。

正如上一章所说,当词法分析器处于"正在读取注释"状态时,其只希望看到"*",如果看到了,则词法分析器的状态就应转入"正在离开注释"状态,否则,什么都没有变,词法分析器将仍然处于"正在读取注释"状态。

同理,当词法分析器处于"正在离开注释"状态时,如果其看到的是"/",则词法分析器就成功离开了注释,其状态就回到了"开始状态";而如果其看到的还是"*",则词法分析器将继续停留在"正在离开注释"状态;否则,很可惜,词法分析器应退回到"正在读取注释"状态。

此外,由于在读取注释的过程中可能发生换行,故我们不要忘了在这两个函数中处理行数的问题。

8. "正在读取小于号/大于号/等号/不等号"状态的分派函数的实现

__nextTokenInLessStage、__nextTokenInGreaterStage、__nextTokenInAssignStage、__nextTokenInNotStage函数分别用于在词法分析器处于"正在读取小于号"、"正在读取大于号"、"正在读取等号"和"正在读取不等号"状态时被调用,其实现如下:

void __LexicalAnalyzer::__nextTokenInLessStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
    string &tokenStr, int &)
{
    lexerStage = __LexerStage::__Done;

    if (*codePtr == '=')
    {
        tokenType = __TokenType::__LessEqual;
        tokenStr += *codePtr++;
    }
    else
    {
        tokenType = __TokenType::__Less;
    }
}


void __LexicalAnalyzer::__nextTokenInGreaterStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
    string &tokenStr, int &)
{
    lexerStage = __LexerStage::__Done;

    if (*codePtr == '=')
    {
        tokenType = __TokenType::__GreaterEqual;
        tokenStr += *codePtr++;
    }
    else
    {
        tokenType = __TokenType::__Greater;
    }
}


void __LexicalAnalyzer::__nextTokenInAssignStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
    string &tokenStr, int &)
{
    lexerStage = __LexerStage::__Done;

    if (*codePtr == '=')
    {
        tokenType = __TokenType::__Equal;
        tokenStr += *codePtr++;
    }
    else
    {
        tokenType = __TokenType::__Assign;
    }
}


void __LexicalAnalyzer::__nextTokenInNotStage(const char *&codePtr, __LexerStage &lexerStage, __TokenType &tokenType,
    string &tokenStr, int &lineNo)
{
    if (*codePtr == '=')
    {
        lexerStage = __LexerStage::__Done;
        tokenType = __TokenType::__NotEqual;
        tokenStr += *codePtr++;
    }
    else
    {
        __invalidChar(*codePtr, lineNo);
    }
}

这几个函数的实现思路都是一致的,其均用于处理类似于:当前记号是一个"<"还是一个"<="的矛盾。我们不妨以"正在读取小于号"为例,来看看这几个函数的实现。

当词法分析器已经读取到了一个"<"后,其进入"正在读取小于号"状态。在此状态下,无论当前读取到的字符是什么,词法分析器都一定会进入"完成"状态。我们只需要看看当前读取到的字符是不是一个"=",如果是,则我们就读取到了一个"<=";如果不是,则我们就只是读取到了一个"<"。

略有不同的是,当词法分析器读取到一个"!",然后进入"正在读取不等号"状态后,其必须继续读取到一个"=",以构成"!=";而如果此时读取到的字符并不是"=",则将被认为是一个语法错误。

至此,"前端中的前端"——词法分析器,就已经全部实现完成了。接下来,我们需要为实现编译器前端的第二个组件——语法分析器做准备。请看下一章:《实现语法分析器前的准备》。

posted @ 2021-02-19 16:26  樱雨楼  阅读(446)  评论(0编辑  收藏  举报