提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、Kaleidoscope
提示:这里可以添加本文要记录的大概内容:
本章节朱啊哟介绍一种sub编程语言——Kaleidoscope。该语言支持函数定义、条件语句、数学运算等。后续我们对该语言的语言特征进行扩展,支持if/then/else
语句、for
循环、自定义操作符、JIT编译、调试信息等。
为了简化数据类型,我们设计的Kaleidoscope只有一种数据类型(64位浮点类型),即C语言中的double
类型。因此所有值都是隐式双进度类型,并且无需进行类型申明。如下所示,为基于Kaleidoscope的斐波拉契计算方法。
def fib(x)
if x< 3 then
1
else
fib(x-1) + fib(x-2)
fib(40)
此外,我们将支持Kaleidoscope调用标准库函数。因此,我们可以在使用函数之前使用extern关键字来定义函数。例如:
extern sin(arg);
extern cos(arg);
extern atan2(arg1 arg2);
atan2(sin(.4), cos(42)
实现编程语言的本质其实是实现编译程序语言的编译器
下面将开始为Kaleidoscope语言逐步实现编译器
二、词法分析器
first of all,我们要实现编译器的文本处理和内容识别能力,即词法分析器(Lexer)。词法分析器将输入内容分解为token, 词法分析器返回的每一个token都包含相关元数据,如:token值、token类型等。如下所示,为token的定义类型。
// The lexer returns tokens [0-255] if it is an unknown character, otherwise one
// of these for known things.
enum Token {
tok_eof = -1,
// commands
tok_def = -2,
tok_extern = -3,
// primary
tok_identifier = -4,
tok_number = -5,
}
static std::string IdentifierStr; // Filled in if tok_identifier
static double NumVal; // Filled in if tok_number
此法分析器返回的token类型有两大类:
- 预定义类型,即
Token
枚举中定义的枚举值,使用负整数表示。 - 未知字符,如
+
,使用[0-255]正整数表示
当词法分析器解析到tok_identifier
类型或者tok_number
类型的token时,会进行以下处理。
- 如果当前token类型是
tok_identifier
时,则使用全局变量IdentifierStr
保存标识符的名称,作为token的值。 - 如果当前token类型是
tok_number
,则使用全局变量NumVal
保存数值,作为token值。
注意,这里其实与LEX工具使用yyval保存token值,yytext保存标识符有着异曲同工之妙。
此法分析器的核心部分由gettok
函数实线。每一次调用gettok
函数都将从标准输入中返回下一个token。gettok
的具体实线如下所示。
#include <stdio.h>
#include <ctype.h>
#include "token.h"
///gettok -Return the next token from standard input.
static int gettok(){
static int LastChar = ' ';
// Skip any whitespace
while(isspace(LastChar))
LastChar = getchar();
// identifier : [a-zA-Z][a-zA-Z0-9]*
if (isalpha(LastChar)){
IdentifierStr = LastChar;
while (isalnum(LastChar = getChar()))
IdentifierStr += LastChar;
if(IdentifierStr == "def")
return tok_def;
if(IdentifierStr == "extern")
return tok_extern;
return tok_identifier;
}
if(isdigit(LastChar) || LastChar = '.'){
std::string NumStr;
do{
NumStr += LastChar;
LastChar = getchar();
}while (isdigit(LastChar) || LastChar == '.');
NumVal = strtod(NumStr.c_str(), nullptr);
return tok_number;
}
if (LastChar == '#') {
// Comment until end of line.
do
LastChar = getchar();
while (LastChar != EOF && LastChar != '\n' && LastChar != '\r');
if (LastChar != EOF)
return gettok();
}
// Check for end of file. Don't eat the EOF.
if (LastChar == EOF)
return tok_eof;
// Otherwise, just return the character as its ascii value.
int ThisChar = LastChar;
LastChar = getchar();
return ThisChar;
}
gettok
函数使用C函数的getChar()
读取标准输入字符,并通过while
循环来忽略token之间的空格。然后,根据第一个字符进行分类处理。
- 如果第一个字符是字母,那么
gettok
开始识别标识符和关键字,并将标识符存入IdentifierStr
全局变量,返回对应的token类型。 - 如果第一个字符是数字或
.
,那么gettok
开始识别数值,并将数值存入NumVal
全局变量,返回token类型tok_number
- 如果第一个字符是
#
,那么gettok
识别注释,该符号之后郑航内容均为注释内容,进行忽略处理。 - 如果第一个字符是
EOF
,那么识别为文件结束符,返回token类型token_eof
。 - 其他情况,返回该字符的ASCII值。
如下所示,为gettoken
函数的工作原理示意图。
至此,我们已经实现了Kaleidoscope的词法分析器。下一章,我们将实现语法分析器(Parser),从而构建抽象语法树(Abstract Syntax Tree)。当实现了词法分析器和语法分析器后,我们会实现一个驱动器(Driver)来使两者协同进行工作,并进行测试。