在这一章的旅程中,咱们将要为整个编译器的“前端中的前端”:词法分析器的实现作好充足的准备。前端
纵观编译器的输入:源代码,咱们不难发现,源代码说白了也就是一个很长很长的字符串。而说到字符串,咱们不难想到字符串的分割函数。这类分割函数以空格,或任意的什么字符或字符串做为分隔符,将一个字符串分割成多个小字符串片断。这不就是词法分析器么?你可能会想。可是,咱们将遇到这样的问题:算法
"1 + 1" -> ("1", "+", "1") "1+1" -> ?
确实,使用普通的字符串分割函数能够很轻易的将上面第一个字符串进行分割,但咱们发现,不管怎么设置分隔符,咱们都没法将第二个字符串也分割成一样的结果了。也就是说,普通的字符串分割函数及其算法是不能胜任词法分析器的工做的,咱们必须另想办法。函数
要想分割一个字符串,其思路无非就是寻找一个分割点,而后将当前起点到分割点的这段字符串分割出去,再将当前起点设置于分割点以后,并继续寻找下一个分割点,不断重复这个过程,直至到达字符串的结尾。那么,为何字符串分割函数不能胜任词法分析器的工做呢?略加思索不难发现缘由:字符串分割函数的“寻找下一个分割点”的逻辑过于简单了,只是一个相等性判断。而咱们所须要的逻辑更复杂,好比:看到一个空格,就分割;再好比:看到一个不是数字的字符,就分割;等等。因此,只要咱们扩充字符串分割函数的“寻找下一个分割点”的逻辑,咱们就能实现出词法分析器了。code
咱们首先须要作什么呢?咱们须要为词法分析器定义许多不一样的状态,处于不一样状态的词法分析器执行不一样的行为。显然,词法分析器须要一个开始状态,一个完成状态,其可能还须要一个或多个中间状态。词法分析器从开始状态开始,不断读取源代码中的每一个字符,最终结束于完成状态,当词法分析器处于完成状态时,其就分割出了一个记号。词法分析器不断执行这样的“开始, ..., 完成”过程,直至到达字符串的结尾。token
为了获知词法分析器到底须要哪些状态,咱们须要看一看CMM语言对于记号的定义。请注意,这里的记号是广义的,其不只表明一个英文单词,还表明一个符号,一串数字等,即,一个记号就是词法分析器须要分割出来的一段字符串。CMM语言对于记号的定义以下所示:字符串
这里须要说明的是:所谓关键词,仅仅是上述第1条的一种特例。即:当咱们分割出一个单词时,咱们须要额外断定一下这个单词是否是关键词,若是是,则咱们须要将这个单词的类别从“单词”变为“关键词XX”。例如:当咱们分割出字符串“abc”时,咱们将其归类为“单词”;而当咱们分割出字符串“if”时,咱们就须要将其归类为“关键词if”。编译器
有了CMM语言对于记号的定义,咱们就能够着手考虑词法分析器到底须要哪些状态了。咱们不妨以上述第一条规则为例进行思考,即:为了分割出一个单词,词法分析器须要哪些状态?string
首先,词法分析器从“开始”状态开始,若是此时词法分析器读入了一个大写或小写字母,则咱们知道:接下来读取到的将是一个单词了;但同时,仅凭读取到的这个字符,咱们永远不可能知道当前的这个单词是否已经读取结束;咱们只有看到一个不是大写或小写字母的字符时,才能肯定刚刚的这个单词已经读取结束了,咱们应令词法分析器进入“完成”状态。为了处理这种状况,咱们引入中间状态“正在读取单词”。当词法分析器读入一个大写或小写字母时,其应当即由“开始”状态转入“正在读取单词”状态;词法分析器应保持这个状态,并不断读入新的字符,直至当前读入的字符不是大写或小写字母,此时,词法分析器应当即由“正在读取单词”状态转入“完成”状态,完成这次分割。it
那么,如何利用上述思路,使词法分析器跳过注释呢?请看:编译
首先,词法分析器仍是从“开始”状态开始,当其读入一个“/”时,咱们此时并不知道这个“/”是一个除号,仍是注释的开始,故咱们先令词法分析器进入“正在读取除号”这个中间状态。在此状态中,若是词法分析器读入的下一个字符是一个“”,则此时咱们就能够肯定词法分析器如今进入了注释中,咱们就再令词法分析器转入“正在读取注释”状态;反之,若是词法分析器读入的下一个字符不是一个“”,咱们也能够肯定词法分析器此次读取到的真的是一个除号,此时,咱们固然是令词法分析器进入“完成”状态。
当词法分析器处于“正在读取注释”状态中时,咱们须要关注两件事:
怎么逃离注释呢?显然,若是要逃离注释,咱们就须要同时知足这两个条件:
因此,当词法分析器被困在注释中时,其一边一视同仁的丢掉一切读取到的字符,一边也留心着读取到的字符是否是“”,若是是,词法分析器就看到了但愿。此时,词法分析器应转入“正在逃离注释”状态,在这个状态下,若是词法分析器又读取到了“/”,那么恭喜,此时词法分析器就成功的逃离了注释,又回到了久违的“开始”状态;若是不是“/”,但愿也没有彻底破灭,此时,若是词法分析器读取到的仍是“”,那么其就还应该停留在“正在逃离注释”状态;而若是读取到的既不是“/”也不是“*”,那么很遗憾,逃离就完全失败了,词法分析器又将回退到“正在读取注释”状态。
利用上述思路触类旁通,咱们便可获得词法分析器所须要的全部状态了。请看:
至此,咱们就获得了词法分析器所须要的全部状态。代码以下所示:
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, };
当词法分析器读取到一个记号后,咱们就须要将其进行归类。有了词法分析器的各类状态的辅助,这样的归类将变的十分容易。例如,当咱们从“正在读取数字”状态转移至“完成”状态时,咱们固然知道当前的这个记号的类别是“数字”;而当咱们读取到一个“(”时,咱们固然也知道这个记号的类别是“左圆括号”;以此类推。咱们能够从上文中给出的记号的定义中,获得全部记号的类别。代码以下所示:
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节点类别,与词法分析器无关。咱们将在后续的旅程中讲述这部分类别的做用。
在实现词法分析器以前,咱们还有一些比较简单的准备工做须要作,列举以下:
struct Token { // Attribute TOKEN_TYPE tokenType; string tokenStr; int lineNo; };
在这个结构体中,咱们保存了记号的类别、记号字符串,以及这个记号在源代码中所处的行数。
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}, };
经过键的存在性检测,咱们就能够断定一个单词是不是一个关键词了;若是是,咱们也能够获得这个关键词所对应的记号的类别。
void InvalidChar(char invalidChar, int lineNo) { printf("Invalid char: %c in line: %d\n", invalidChar, lineNo); exit(1); }
至此,咱们就完成了全部准备工做,能够开始实现词法分析器了。请看下一章:《实现词法分析器》。