编译技术教程:词法分析
系列导航: 《编译技术教程:前言》 《编译技术教程:词法分析》 《编译技术教程:抽象语法树》 《编译技术教程:语法分析》 《编译技术教程:符号表与类型系统》 《编译技术教程:语义分析》 《编译技术教程:附录 - 项目测试》 不论是从标准输入中读取还是从文件中解析,我们的源程序总是以字符流的形式存在。例如,对于下面这个简单的源程序: int main() { int var = 8 + 2; if (var != 10) { printf("error!"); } else { printf("correct!"); } return 0; } 尽管从我们的视角来看,源程序是结构化的,包含了顺序、分支、循环等多种结构;同时通过空格、换行等方法,我们还可以将程序中的各个成分清晰地区分开来。但是从编译器的角度看,源程序不带有任何结构,只是由字符组成的序列(字符串): int main() {\n\tint var = 8+2;\n\tif (var != 10) {\n\t\tprintf(“error!”);\n\t}\n\telse {\n\t\tprintf(“correct!”);\n\t}\n\treturn 0;\n} 这导致了一个问题,虽然字符是构成字符串的基本单元,却每个字符自身却不一定具备特定的含义。这些字符需要组成一个单词才能传达出对于程序来说有意义的信息。因此,实现编译器的第一步就是要把这样的线性字符串分割成一个个单词,便于后续分析。在编译器中,这一阶段被称为词法分析。 一、词法分析作用 词法分析器作为编译器的第一部分,承担的任务就是通过扫描输入的源程序字符串,将其分割成一个个单词。如图所示,经过词法分析器的处理,我们将字符序列转换为单词序列。对于每个单词,我们至少应当记录单词的取值及其类别信息。另外,编译器在词法分析阶段也可能记录单词在源代码中的位置信息,例如源文件路径、行号、列号等等。这些额外信息对于编译器的错误处理十分重要。得当的错误定位能够为代码的编写者提供极大便利。 在源程序中,还有一些字符并不会影响程序的语法语义,如换行符 \n、注释等,这些符号自然也不会被词法分析器解析为单词,但词法分析器也需对这些符号进行适当处理,如忽略跳过、记录行号列号等等。 二、词法分析器的接口 在上一小节中,我们了解了词法分析阶段的主要作用,在本小节中,我们将基于此介绍词法分析器(Lexer)。 首先,词法分析的输出是单词序列,因而我们需要定义单词的数据结构。在 tolangc 中,我们所实现的 Token 类包括三个字段:type、content 和 lineno,分别对应了单词的类型、取值和位置。 struct Token { enum TokenType { #define X(a, b) a, TOKEN_TYPE #undef X } type; std::string content; int lineno; Token() = default; Token(TokenType type, std::string content, int lineno) : type(type), content(content), lineno(lineno) {} }; 对于单词类型,除了文法定义中出现的类型外,我们还额外定义了 TK_EOF 类型。这一类型用于表示我们已经读到了输入字符串的末尾。...