要实现一门语言,第一要务就是要能够处理文本文件,搞明白其中究竟写了些什么。传统上,我们会先利用词法分析器(也称为“扫描器”)将输入切成语元(token),然后再做处理。词法分析器返回的每个语元都带有一个语元编号,此外可能还会附带一些元数据(比如某个数值)。
首先,我们把所有可能出现的语元都定义出来:
// The lexer returns tokens [0-255] if it is an unknown character, |
我们的词法分析器返回的语元,要么是上述若干个语元枚举值之一,要么是诸如+
这样的“未知”字符。对于后一种情况,词法分析器返回的是这些字符的ASCII值。如果当前语元是标识符,其名称将被存入全局变量IdentifierStr
。如果当前语元是数值常量(比如1.0
),其值将被存入NumVal
。注意,简单起见,我们动用了全局变量,在真正的语言实现中这可不是最佳选择🙂。
Kaleidoscope的词法分析器由一个名为gettok
的函数实现。调用该函数,就可以得到标准输入中的下一个语元。它的开头是这样的:
/// gettok - Return the next token from standard input.
static int gettok() {
static int LastChar = ' ';
// Skip any whitespace.
while (isspace(LastChar))
LastChar = getchar();
gettok
通过C标准库的getchar()
函数从标准输入中逐个读入字符。它一边识别读取的字符,一边将最后读入的字符存入LastChar
,留待后续处理。这个函数干的第一件事就是利用上面的循环剔除语元之间的空白符。
接下来,gettok
开始识别标识符和def
、extern
等关键字。这个任务由下面的循环负责,代码很简单:
if (isalpha(LastChar)) { // identifier: [a-zA-Z][a-zA-Z0-9]* |
注意,标识符一被识别出来就被存入全局变量IdentifierStr
。此外,语言中的关键字也由这个循环负责识别,在此处一并处理。数值的识别过程与此类似:
if (isdigit(LastChar) || LastChar == '.') { // Number: [0-9.]+ |
处理输入字符的代码简单明了。只要碰到代表数值的字符串,就用C标准库中的strtod
函数将之转换为数值并存入NumVal
。注意,这里的错误检测并不完备:这段代码会将1.23.45.67
错误地识别成1.23
。改还是不改,那就随你的便了🙂。下面我们来处理注释:
if (LastChar == '#') { |
注释的处理很简单:直接跳过注释所在的那一行,然后返回下一个语元即可。最后,如果碰到上述情况都处理不了的字符,那么只有两种可能:要么碰到了表示运算符的字符(比如+
),要么就是已经读到了文件末尾。这两种情况由以下代码负责处理:
// Check for end of file. Don't eat the EOF. |
至此,完整的Kaleidoscope词法分析器就完成了。接下来,我们将编写一个简单的语法分析器并利用它来构建抽象语法树。届时,我们还会再加上一段引导代码,让词法分析器和语法分析器珠联璧合、天衣无缝。