LLVM项目教程:实现Kaleidoscope语言的解析器与抽象语法树
前言
在编译器开发领域,解析器和抽象语法树(AST)是构建编程语言前端的关键组件。本文将基于LLVM项目中的Kaleidoscope教程,深入讲解如何为自定义语言构建解析器和AST。这是继词法分析器之后的第二步,我们将学习如何将词法单元转换为有意义的语法结构。
抽象语法树(AST)设计
AST是源代码的树状表示,它抽象掉了语法细节,只保留程序的结构和语义信息。在Kaleidoscope语言中,我们需要为各种语言结构设计AST节点。
表达式基类
class ExprAST {
public:
virtual ~ExprAST() {}
};
这是所有表达式节点的基类,使用虚析构函数确保派生类能被正确销毁。
具体表达式节点
- 数值字面量节点:
class NumberExprAST : public ExprAST {
double Val;
public:
NumberExprAST(double Val) : Val(Val) {}
};
存储浮点数值,如1.0
。
- 变量引用节点:
class VariableExprAST : public ExprAST {
std::string Name;
public:
VariableExprAST(const std::string &Name) : Name(Name) {}
};
表示对变量的引用,如x
。
- 二元运算符节点:
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)) {}
};
表示二元运算,如x + y
。
- 函数调用节点:
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)) {}
};
表示函数调用,如sin(x)
。
函数相关节点
- 函数原型节点:
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; }
};
表示函数声明,包含函数名和参数列表。
- 函数定义节点:
class FunctionAST {
std::unique_ptr<PrototypeAST> 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)) {}
};
组合函数原型和函数体,表示完整的函数定义。
解析器实现
解析器负责将词法单元流转换为AST,我们采用递归下降解析方法。
基础工具函数
- 令牌缓冲:
static int CurTok;
static int getNextToken() {
return CurTok = gettok();
}
提供单令牌前瞻能力。
- 错误处理:
std::unique_ptr<ExprAST> LogError(const char *Str) {
fprintf(stderr, "Error: %s\n", Str);
return nullptr;
}
统一的错误报告机制。
基本表达式解析
- 数值字面量解析:
static std::unique_ptr<ExprAST> ParseNumberExpr() {
auto Result = std::make_unique<NumberExprAST>(NumVal);
getNextToken(); // 消耗数字令牌
return std::move(Result);
}
- 括号表达式解析:
static std::unique_ptr<ExprAST> ParseParenExpr() {
getNextToken(); // 消耗'('
auto V = ParseExpression();
if (!V) return nullptr;
if (CurTok != ')')
return LogError("expected ')'");
getNextToken(); // 消耗')'
return V;
}
处理括号分组,如(1+2)*3
。
- 标识符解析:
static std::unique_ptr<ExprAST> ParseIdentifierExpr() {
std::string IdName = IdentifierStr;
getNextToken(); // 消耗标识符
if (CurTok != '(') // 简单变量引用
return std::make_unique<VariableExprAST>(IdName);
// 函数调用处理
getNextToken(); // 消耗'('
std::vector<std::unique_ptr<ExprAST>> Args;
// ... 参数解析逻辑
return std::make_unique<CallExprAST>(IdName, std::move(Args));
}
区分变量引用和函数调用。
二元表达式解析
处理运算符优先级是解析器的核心挑战。我们采用运算符优先级解析算法。
- 优先级表:
static std::map<char, int> BinopPrecedence = {
{'<', 10}, {'+', 20}, {'-', 20}, {'*', 40}
};
定义各运算符的优先级。
- 表达式解析入口:
static std::unique_ptr<ExprAST> ParseExpression() {
auto LHS = ParsePrimary();
if (!LHS) return nullptr;
return ParseBinOpRHS(0, std::move(LHS));
}
- 右结合处理:
static std::unique_ptr<ExprAST> ParseBinOpRHS(int ExprPrec,
std::unique_ptr<ExprAST> LHS) {
while (true) {
int TokPrec = GetTokPrecedence();
if (TokPrec < ExprPrec) return LHS;
int BinOp = CurTok;
getNextToken(); // 消耗运算符
auto RHS = ParsePrimary();
if (!RHS) return nullptr;
// 处理更高优先级的运算符
int NextPrec = GetTokPrecedence();
if (TokPrec < NextPrec) {
RHS = ParseBinOpRHS(TokPrec+1, std::move(RHS));
if (!RHS) return nullptr;
}
LHS = std::make_unique<BinaryExprAST>(BinOp, std::move(LHS),
std::move(RHS));
}
}
这段代码实现了运算符优先级的正确处理,确保1+2*3
被正确解析为1+(2*3)
。
总结
本文详细介绍了如何为Kaleidoscope语言构建解析器和AST。关键点包括:
- 设计了完整的AST类层次结构,覆盖语言的各种语法结构
- 实现了递归下降解析器,处理基本表达式
- 使用运算符优先级解析算法正确处理二元表达式
- 建立了完善的错误处理机制
这些技术不仅适用于Kaleidoscope语言,也是构建任何编程语言前端的通用方法。通过本教程,读者可以掌握编译器前端开发的核心技术,为后续的语义分析和代码生成打下坚实基础。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考