Kaleidoscope: Implementing a Parser and AST

Introduction

本章将使用上一章中的词法分析器喂Kaleidoscope构建完整的解析器, 然后定义并构建一个Abstract Syntax Tree (抽象语法树).
解析器使用递归下降解析和操作符优先解析的组合来解析Kaleidoscope.我们先从解析器的输出,AST开始.

AST 抽象语法树

程序的AST应该使编译器的后续阶段容易理解. 我们希望每个构造都有一个对象,AST应该对语言进行密切建模.在Kaleidoscope中,我们有表达式,原型, 和函数对象. 我们首先从表达式开始:

// ExprAST - 所有表达式节点的基类
classs ExprAST{
publlic:
	virtual ~ExprAST(){}
};

// NumberExprAST - 数值表达式,如''1.0'
class NumberExprAST : public ExprAST{
	double Val;
	
public: 
	NumberExprAST(double Val) : Val(Val){}
}

NumberExprAST类将数值捕捉为实例变量, 这允许编译器的后续阶段获取存储的数值.
目前我们只是创建AST, 还没有访问它们的有效方法, 我们可以添加虚拟方法来打印代码.
以下是其他表达式AST节点定义:

// VariableExprAST - 变量的表达类, 如 'a'
class VariableExprAST : public ExprAST{
	std::string Name;

public:
	VariableExprAST(const std::string &Name) : Name(Name){}
};

// BinaryExprAST - 二元操作符表达类
class BinaryExprAST :  public ExprAST{
	char Op;
	std::unique_ptr<ExprAST>  LHS, RHS;

public:
	BinaryExprAST(char op, std::unique_ptr<ExprAST> LHS, std::unique_ptr<ExprAST> RHS) : Op(op), LHS(std::move(LHS)), RHS(std::move(RHS)){}
};

//CallExprAST - 函数调用表达类
class CallExprAST : public ExprAST{
	std::string Callee;
	std::vector<std::unique_ptr<ExprAST>> Args;

public:
	CallExprAST(const std::string &Callee, std::vector<std::unique_ptr<ExprAST>> 						Args):Callee(Callee), Args(std::move(Args)){}
};

这些都非常直接 : 变量捕捉变量名称,二元运算符捕捉它们的操作码(例如’+’), 调用则捕捉函数名称和参数列表.请注意, 这里没有讨论二元运算符的优先级和此法结构.
其中,move函数视为性能而生的, std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝,避免了不必要的拷贝操作.
例如:

#include<iostream>
#include<utility>
#include<vector>
#include<string>
int main(){
	std::string str = "hello";
	std::vector<std::string> v;
	//调用常规的拷贝构造函数, 新建字符数组, 拷贝数据
	v.push_back(str);
	std::cout<<"after copy, str is : "<<str<<endl;
	//调用移动构造函数, 掏空str, 最好不要使用str
	v.push_back(std::move(str));
	std::cout<<"after move, str is : "<<str<<endl;
}

接下来,需要讨论的是函数接口, 以及函数本身

//PrototypeAST - 这个类表示函数的"原型", 它捕捉函数名称,及其参数名称, 因此隐含了函数所需的参数数量
class PrototypeAST{
	std::string Name;
	std::vector<std::string> Args;

public:
	PrototypeAST(const std::string &name, std::vector<std::string> Args) : Name(name), Args(std::move(Args)){}

	const std::string &getName() const{
		return Name;
	}
};

//FunctionAST - 这个类用来表示函数本身的定义
class FunctionAST{
	std::unique_ptr<PrototttypeAST> Proto;
	std::unique_ptr<ExprAST> Body;

public:
	FunctionAST(std::unique_ptr<PrototypeAST> Proto, std::unique_ptr<ExprAST> Body) : 	Proto(std::move(Proto)), Body(std::move(Body)){}
};

Kaleidoscope中, 函数只需要记录参数. 参数值都是双精度浮点数,所以不需要再存储其类型. 但是在其它语言中, "ExprAST"类可能需要有一个类型字段.
有了这个支架,现在我们可以讨论解析表达式和函数体.

Parser Basics 语法分析器基础

现在我们需要构建一个AST, 我们需要定义语法分析器来构建它. 我们要解析类似"x+y"(由词法分析器返回三个token)到AST中, 这可以通过这样的调用生成:

auto LHS = llvm::make_unique<VariableExprAST>("x");
auto EHS = llvm:make_unique<VariableExprAST>("y");
auto Result = std::make_unique<BinaryExprAST>('+', std::move(LHS), std::move(RHS));

为此, 首先我们定义一些基本的帮助程序

//提供了一个简单的token缓冲区, CurTok是当前语法分析器访问的token, getNextToken从词法分析器中读下一个token, 并更新CurTok
static int CurTok;
static int getNextToken(){
	return CurTok = gettok();
}

这围绕着词法分析器实现了一个简单的token缓冲区, 允许我们提前查看词法分析器的返回值. 我们解析器的每一个函数都假设CurTok是当前被解析的token.

//LogError* - 这些是错误处理的小的帮助程序
std::unique_ptr<ExprAST> LogError(const char *Str){
	fprintf(stderr, "LogError: %s\n", Str);
	return nullptr;
}
std::unique__ptr<PrototypeAST> LogErrorP(const char *Str){
	LogError(Str);
	return nullptr;
}

该LogError是用来处理错误的简单辅助程序, 使得在具有各种返回类型的例程中总是返回空指针.
其中,在编译器进行解释程序时,nullptr在C++11中就是代表空指针,不能被转换成数字. 而NULL会被直接解释成0.
有了这些基本的辅助函数, 我们就可以实现语法的第一部分: 数值

Basic Expression Parsing 基本表达式解析

我们从数值开始, 因为它们是最简单的处理方式.对于语法中的每一部分, 我们定义一个解析它的函数.对于数值, 我们有:

//numberexpr ::= number
static std::unique_ptr<ExprAST> ParseNumberExpr(){
	auto Result = llvm::make_unique<NumberExprAST>(NumVal);
	getNextToken();
	return std::move(Result);
}

该例程希望当前token为tok_number时被调用, 获取当前数值, 创建NumberExprAST节点, 将词法分析器移到下一个token, 并返回.
重要的是, 该例程会占用与该production现骨干的所有token, 并返回带有下一个token的词法分析器缓冲区, 这是递归下降解析器的一种标准方法.更好的示例, 括号运算符的定义如下:

//parenenpr ::= '(' expression ')'
static std::unique_ptr<ExprAST> ParseParenExpr(){
 	getNextToken();   // eat (
 	auto V = ParseExpression();
 	if (!V)
 		return nullptr;
 	
 	if (CurTok != ')')
 		return LogError("expected ')'");
 	getNextToken();    // eat )
 	return V;
}

这个函数说明了解析器的一些有趣的东西:
1). 它显示了我们如何使用LogError例程. 如果用户输入的是"(4x"而不是"(4)", 解析器应该发出错误, 这里我们返回空指针.
2). 它通过调用递归ParseExpression(很快可以看到它, ParseExpression可以调用ParseParenExpr). 这很强大, 因为它允许我们处理递归语法, 并使得每个production变得非常简单.
下一个简单的production是用于处理变量引用和函数调用:

//identifierexpr
// ::= identifier
// ::= identifier '(' expression* ')'
static std::unique_ptr<ExprAST> ParseIdentifierExpr(){
	std::string IdName = IdentifierStr;

	getNextToken();  //  eat identifier

	if (CurTok != '(')    // Simple variable ref
		return llvm::make_unique_ptr<VariableExprAST>(IdName);

	// Call
	getNextToken();    // eat (
	std::vector<std::unique_ptr<ExprAST>> Args;
	if (CurTok != ')'){
		while(1){
			if (auto Arg = ParseExpression())
				Args.push_back(std::move(Arg));
			else
				return nullptr;
			
			if (CurTok == ')')
				break;
			
			if (CurTok != ',')
				return LogError("Expected ')' or ',' in argument list");
			getNextToken();
		}
	}

	// eat )
	getNextToken();

	return llvm::make_unique<CallExprAST>(IdName, std::move(Args));
}

此例程遵循与其他例程相同的样式,如果当前token是tok_identifier, 则被调用. 它还有递归和错误处理. 它使用look-ahead, 以确定当前token是否是一个独立变量, 或者是一个函数调用的表达. 它通过检查标识符后面的标记是否是’)’, 根据需要构造一个VariableExprAST或者一个CallExprAST节点来处理问题.
现在我们已经拥有了所有简单表达式解析逻辑,我们可以定义一个辅助函数将它们组合成一个入口点. 我们称这类表达式为"主要"表达式. 为了解析任意的主表达式, 我们需要确定它是什么类型的表达式:

//primary
// ::= identifierexpr
// ::= numberexpr
// ::= parenexpr
static std::unique_ptr<ExprAST> ParsePrimary(){
	switch (CurTok){
		default:
			return LogError("unkonwn token when expecting an expression");
		case tok_identifier:
			return ParseIdentifierExpr();
		case tok_number:
			return ParseNumberExpr();
		case '(':
			return ParseParenExpr();
	}
}

看到这个函数的定义后, 我们可以明显的理解为什么在各个函数中假设CurTok的状态. 这使用look-ahead来确定正在检查哪种表达式, 然后使用函数调用对其进行解析.
现在处理了基本表达式, 我们需要处理二进制表达式. 它们有点复杂.

Binary Expression Parsing 二进制表达式解析

二进制表达式很难解析, 因为它们通常是模糊的. 例如, 当给定字符串"x+yz"时, 解析器可以选择将其解析为"x+y" * z, 或 "x + (yz)". 我们通常期望后面的解析, 因此"*“具有比”+"更高的优先级.
这里使用的是Operator-Precedence Parsing 运算符优先级解析器, 使用二元运算符的优先级来指导递归. 首先, 我们需要一个优先级表:

//它保存了定义过的每个二元操作符的优先级
static std::map<char, int> BinoPrecedence;

//获得挂起的二元操作符token的优先级
static int GetTokPrecedence(){
	if (!isascii(CurTok))
		return -1;
	
	//确保是被声明过的二元操作符
	int TokPrec = BinopPrecedence[CurTok];
	if (TokPrec <= 0) 
		return -1;
	return TokPrec;
}

int main(){
	//保存标准的二元操作符
	// 1 是最低优先级
	BinopPrecedence['<'] = 10;
	BinopPrecedence['+'] = 20;
	BinopPrecedence['-'] = 20;
	BinopPrecedence['*'] = 40;
	...
}

对于Kaleidoscope的基本形式, 我们只支持四个二元运算符. 该GetTokPrecedence函数返回当前token的优先级, 如果token不是二元运算符, 则返回-1. 使用map可以轻松添加新运算符, 并清楚地表明算法不依赖于所涉及的特定运算符.
现在我们可以开始解析二进制表达式.运算符优先级解析的基本思想是将具有可能不明确的二元运算符的表达式分解成多个部分. 例如, 考虑表达式"a+b+(c+d)ef+g". 运算符优先级解析将此视为由二元运算符分隔的主表达式流. 因此, 它首先解析主要的表达式"a", 然后它将看到对[+,b], [+, (c+d)][*, f], [+, g]. 请注意, 括号是主表达式, 所以二进制表达式根本不需要担心嵌套的子表达式, 如(c+d).
首先, 表达式是一个主表达式, 后面可能跟一系列的[binop, primaryexpr]对:

// expression
//  ::= primary binoprhs
static std::unique_ptr<ExprAST> ParseExpression(){
	auto LHS = ParsePrimary();
	if (!LHS)
		return nullptr;

	return ParseBinopRHS(0, std::move(LHS));
}

ParseBinopRHS是我们解析对序列的函数, 它需要一个优先级和一个指向已解析的部分的表达式的指针.
“x"是一个完全有效的表达式, 所以"binoprhs"被允许为空, 在这种情况下, 它返回传递给它的表达式. 在上面的示例中, 代码将"a"的表达式传递给ParseBinopRHS, 当前token为”+".
传入的优先级值ParseBinopRHS表示允许该函数吃的最小运算符优先级. 例如, 如果当前对流是[+, x]并且ParseBinopRHS以40的优先级传递, 则它不会消耗任何标记("+"的优先级为20).

// binoprhs
//    ::= ('+' primary)*
static std::unique_ptr<ExprAST> ParseBinopRHS(int ExprPrec, std::unique_ptr<ExprAST> LHS){
	//如果是一个二元运算符, 获得它的优先级
	while (1) {
		int TokPrec = GetTokPrecedence();

		//If this is a binop that binds at least as tightly as the current binop, consume it, otherwise we are done.
		if (TokPrec <  ExprPrec)
			return LHS;

此代码获取当前token的优先级, 并检查是否过低. 我们将无效标记定义为优先级为-1, 所以可以隐式地知道当标记流用完二元运算符时, 对流结束. 如果此检查成功, 我们知道该token是二元运算符, 并且它将包含在此表达式中:

int Binop = CurTok;
getNextToken();      // eat binop

// 解析二元运算符后面的主表达式
auto RHS = ParsePrimary();
if (!RHS)
	return nullptr;

此代码吃掉并记住二元运算符, 然后解析后面的主表达式. 这构建了整个对, 第一个是运行示例的[+, b].
现在我们解析了表达式的左侧, 和一对RHS序列, 我们必须决定表达式关联的方式. 特别是, 我们可以有"(a+b)binop unparsed" 或 “a+(b binop unparsed)”. 为了确定这一点, 我们往后看,以确定"binop"的优先级, 并将其与BinOp的优先级(在这种情况下为"+")进行比较:

// 如果BinOp与RHS的联系, 比与RHS之后的操作符少, 那么下一个操作符将RHS作为它的LHS
// If BinOp binds less tightly with RHS than the operator after RHS, let the pending operator take RHS as its LHS
int NextPrec = GetTokPrecedence();
if (TokPrec < NextPrec){

如果binop在"RHS"右边的优先级低于或者等于我们当前运算符的优先级, 那么我们知道括号关联位"(a+b)binop…". 在我们的示例中, 当前运算符位"+", 下一个运算符为"+", 我们知道它们具有相同的优先级. 在这种情况下, 我们将为"a+b"创建AST节点, 然后继续解析:

		... if (body ommited) ...
		}

		// Merge LHS/RHS
		LHS = llvm::make_unique<BinoryExorAST>(Binop, std::move(LHS), std::move(RHS));
 	}  //  loop around to the top of the while loop
 }	

上面的例子中, 把"a+b+“变成”(a+b)", “+“作为当前token, 并执行循环的下一次迭代. (c+d)作为主表达式, 将被吃掉, 保存, 并解析, 使得当前对为[+, (c+d)]. 然后”"作为右侧的binop, 判断if条件. 此时, "“优先级高于”+”, if成立.
此时有个关键问题, if条件如何完全解析右手边? 解决的代码其实很简单:

		// If BinOp binds less tightly with RHS than the operator after RHS, let the pending operator take RHS as its LHS
		int NextPrec = GetTokPrecedence();
		if (TokPrec < NextPrec){
			RHS = ParseBinopRHS(TOkePrec+1, std::move(RHS));
			if (!RHS)
				reurn nullptr;
		}
		// Merge LHS/RHS
		LHS = llvm::make_unique<BinoryExorAST>(Binop, std::move(LHS), std::move(RHS));
 	}  //  loop around to the top of the while loop
 }	

此时我们知道我们的主要RHS的二元运算符优先于我们当前正在解析的binop, 任何优先于"+“的运算符的对序列都应该被一起解析并作为"RHS"返回. 因此, 我们以递归的方式调用ParseBinopRHS函数, 指定"TokPrec+1"作为其继续所需要的最小优先级. 在上面的举例子中, 这使得”(c+d)ef"作为RHS返回, 然后将其设置为"+“的RHS.
最后, 在while的下一次迭代中, 解析”+g"片段并将其添加到AST.
此时, 我们可以将解析器指向任意标记流并从中构建表达式, 停止在不属于表达式的第一个标记处. 接下来我们需要处理函数定义.

Parsing the Rest

接下来是函数原型的处理. 在Kaleidoscope中, 这些用于使用’extern’的函数声明, 以及函数体定义. 代码非常直接:

// prototype
//    ::= id '(' id* ')'
static std::unique_ptr<PrototypeAST> ParsePrototype(){
	if (CurTok != tok_identifier)
		return LogErrorP("Expected function name in prototype");
	
	std:string FnName = IdentifierStr;
	getNextToken();

	if (CurTok != '(')
		return LogErrorP("Expected '(' in prototype");
	
	// Read the list of argument names
	std::vector<std::string> ArgNames;
	while (getNextToken() == tok_identifier)
		ArgNames.push_back(IdentifierStr);
	if (CurTok != ')')
		return LogErrorP("Expected ')' in prototype");
	
	// success
	getNextToken();  // eat ')'
	
	return llvm::make_unique<PrototypeAST>(FnName, std::move(ArgNames));
}

基于此, 函数定义非常简单, 只是一个原型加上一个表达式来实现正文:

// definition ::= 'def' prototype expression
static std::unique_ptr<FunctionAST> ParseDefinition(){
	getNextToken();  // eat def.
	auto Proto = ParsePrototype();
	if (!Proto) return nullptr;

	if (auto E = ParseExpression())
		return llvm::make_unique<FunctionAST>(std::move(Proto), std::move(E));
	return nullptr;
}

此外, 我们支持’extern’来声明’sin’和’cos’之类的函数, 以及支持用户函数的前向声明. 这些’extern’只是没有身体的原型:

// external ::= 'extern' prototype
static std::unique_ptr<PrototypeAST> ParseExtern(){
	get
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值