编译器实现之旅——第四章 实现词法分析器
在上一章的旅程中,我们讨论了词法分析器的实现思路,我们也为词法分析器的实现做了许多准备工作。现在,就让我们来实现词法分析器吧。
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
- 如果当前读取到的字符是"+"、"-"、"*"、";"、","、"("、")"、"["、"]"、"{"、"}"或EOF这些仅由一个字符构成的记号,则我们可以立即确定当前记号的类别,并令词法分析器立即进入"完成"状态
- 如果当前读取到的字符是"/"、"<"、">"、"="或"!",则词法分析器应进入各中间状态
- 如果当前读取到的字符不满足上述各种情况之一,则报错退出
这里需要额外说明的是,如果当前读取到的字符是一个"/",则我们此时并不知道这个"/"需不需要被保存下来。我们必须等到确定这个"/"是一个除号时,再保存这个"/"。
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);
}
}
这几个函数的实现思路都是一致的,其均用于处理类似于:当前记号是一个"<"还是一个"<="的矛盾。我们不妨以"正在读取小于号"为例,来看看这几个函数的实现。
当词法分析器已经读取到了一个"<"后,其进入"正在读取小于号"状态。在此状态下,无论当前读取到的字符是什么,词法分析器都一定会进入"完成"状态。我们只需要看看当前读取到的字符是不是一个"=",如果是,则我们就读取到了一个"<=";如果不是,则我们就只是读取到了一个"<"。
略有不同的是,当词法分析器读取到一个"!",然后进入"正在读取不等号"状态后,其必须继续读取到一个"=",以构成"!=";而如果此时读取到的字符并不是"=",则将被认为是一个语法错误。
至此,"前端中的前端"——词法分析器,就已经全部实现完成了。接下来,我们需要为实现编译器前端的第二个组件——语法分析器做准备。请看下一章:《实现语法分析器前的准备》。