使用 LLVM 实现一个简单编译器

这篇博客详细介绍了如何使用 LLVM 构建一个简单的 Kaleidoscope 编译器,包括词法分析、解析、代码生成到 LLVM IR、优化、JIT 编译器、SSA 形式、控制流程、用户自定义运算符以及可变变量的支持。通过一系列逐步的步骤,作者展示了如何实现这个过程,并提供了完整的代码和参考资料链接。
摘要由CSDN通过智能技术生成

0fdf6f3b0555f75e0a6b8b5a009b2135.gif

作者:tomoyazhang,腾讯 PCG 后台开发工程师

1. 目标

这个系列来自 LLVM 的Kaleidoscope 教程,增加了我对代码的注释以及一些理解,修改了部分代码。现在开始我们要使用 LLVM 实现一个编译器,完成对如下代码的编译运行。

# 斐波那契数列函数定义
def fib(x)
    if x < 3 then
        1
    else
        fib(x - 1) + fib(x - 2)

fib(40)

# 函数声明
extern sin(arg)
extern cos(arg)
extern atan2(arg1 arg2)

# 声明后的函数可调用
atan2(sin(.4), cos(42))

这个语言称为 Kaleidoscope, 从代码可以看出,Kaleidoscope 支持函数、条件分支、数值计算等语言特性。为了方便,Kaleidoscope 唯一支持的数据类型为 float64, 所以示例中的所有数值都是 float64。

2. Lex

编译的第一个步骤称为 Lex, 词法分析,其功能是将文本输入转为多个 tokens, 比如对于如下代码:

atan2(sin(.4), cos(42))

就应该转为:

tokens = ["atan2", "(", "sin", "(", .4, ")", ",", "cos", "(", 42, ")", ")"]

接下来我们使用 C++来写这个 Lexer, 由于这是教程代码,所以并没有使用工程项目应有的设计:

// 如果不是以下5种情况,Lexer返回[0-255]的ASCII值,否则返回以下枚举值
enum Token {
  TOKEN_EOF = -1,         // 文件结束标识符
  TOKEN_DEF = -2,         // 关键字def
  TOKEN_EXTERN = -3,      // 关键字extern
  TOKEN_IDENTIFIER = -4,  // 名字
  TOKEN_NUMBER = -5       // 数值
};

std::string g_identifier_str;  // Filled in if TOKEN_IDENTIFIER
double g_number_val;           // Filled in if TOKEN_NUMBER

// 从标准输入解析一个Token并返回
int GetToken() {
  static int last_char = ' ';
  // 忽略空白字符
  while (isspace(last_char)) {
    last_char = getchar();
  }
  // 识别字符串
  if (isalpha(last_char)) {
    g_identifier_str = last_char;
    while (isalnum((last_char = getchar()))) {
      g_identifier_str += last_char;
    }
    if (g_identifier_str == "def") {
      return TOKEN_DEF;
    } else if (g_identifier_str == "extern") {
      return TOKEN_EXTERN;
    } else {
      return TOKEN_IDENTIFIER;
    }
  }
  // 识别数值
  if (isdigit(last_char) || last_char == '.') {
    std::string num_str;
    do {
      num_str += last_char;
      last_char = getchar();
    } while (isdigit(last_char) || last_char == '.');
    g_number_val = strtod(num_str.c_str(), nullptr);
    return TOKEN_NUMBER;
  }
  // 忽略注释
  if (last_char == '#') {
    do {
      last_char = getchar();
    } while (last_char != EOF &amp;&amp; last_char != '\n' &amp;&amp; last_char != '\r');
    if (last_char != EOF) {
      return GetToken();
    }
  }
  // 识别文件结束
  if (last_char == EOF) {
    return TOKEN_EOF;
  }
  // 直接返回ASCII
  int this_char = last_char;
  last_char = getchar();
  return this_char;
}

使用 Lexer 对之前的代码处理结果为(使用空格分隔 tokens):

def fib ( x ) if x < 3 then 1 else fib ( x - 1 ) + fib ( x - 2 ) fib ( 40 ) extern sin ( arg )
extern cos ( arg ) extern atan2 ( arg1 arg2 ) atan2 ( sin ( 0.4 ) , cos ( 42 ) )

Lexer 的输入是代码文本,输出是有序的一个个 Token。

3. Parser

编译的第二个步骤称为 Parse, 其功能是将 Lexer 输出的 tokens 转为 AST (Abstract Syntax Tree)。我们首先定义表达式的 AST Node:

// 所有 `表达式` 节点的基类
class ExprAST {
 public:
  virtual ~ExprAST() {}
};

// 字面值表达式
class NumberExprAST : public ExprAST {
 public:
  NumberExprAST(double val) : val_(val) {}

 private:
  double val_;
};

// 变量表达式
class VariableExprAST : public ExprAST {
 public:
  VariableExprAST(const std::string&amp; name) : name_(name) {}

 private:
  std::string name_;
};

// 二元操作表达式
class BinaryExprAST : public ExprAST {
 public:
  BinaryExprAST(char op, std::unique_ptr<ExprAST> lhs,
                std::unique_ptr<ExprAST> rhs)
      : op_(op), lhs_(std::move(lhs)), rhs_(std::move(rhs)) {}

 private:
  char op_;
  std::unique_ptr<ExprAST> lhs_;
  std::unique_ptr<ExprAST> rhs_;
};

// 函数调用表达式
class CallExprAST : public ExprAST {
 public:
  CallExprAST(const std::string&amp; callee,
              std::vector<std::unique_ptr<ExprAST>> args)
      : callee_(callee), args_(std::move(args)) {}

 private:
  std::string callee_;
  std::vector<std::unique_ptr<ExprAST>> args_;
};

为了便于理解,关于条件表达式的内容放在后面,这里暂不考虑。接着我们定义函数声明和函数的 AST Node:

// 函数接口
class PrototypeAST {
 public:
  PrototypeAST(const std::string&amp; name, std::vector<std::string> args)
      : name_(name), args_(std::move(args)) {}

  const std::string&amp; name() const { return name_; }

 private:
  std::string name_;
  std::vector<std::string> args_;
};

// 函数
class FunctionAST {
 public:
  FunctionAST(std::unique_ptr<PrototypeAST> proto,
              std::unique_ptr<ExprAST> body)
      : proto_(std::move(proto)), body_(std::move(body)) {}

 private:
  std::unique_ptr<PrototypeAST> proto_;
  std::unique_ptr<ExprAST> body_;
};

接下来我们要进行 Parse, 在正式 Parse 前,定义如下函数方便后续处理:

int g_current_token;  // 当前待处理的Token
int GetNextToken() {
  return g_current_token = GetToken();
}

首先我们处理最简单的字面值:

// numberexpr ::= number
std::unique_ptr<ExprAST> ParseNumberExpr() {
  auto result = std::make_unique<NumberExprAST>(g_number_val);
  GetNextToken();
  return std::move(result);
}

这段程序非常简单,当前 Token 为 TOKEN_NUMBER 时被调用,使用 g_number_val,创建一个 NumberExprAST, 因为当前 Token 处理完毕,让 Lexer 前进一个 Token, 最后返回。接着我们处理圆括号操作符、变量、函数调用:

// parenexpr ::= ( expression )
std::unique_ptr<ExprAST> ParseParenExpr() {
  GetNextToken();  // eat (
  auto expr = ParseExpression();
  GetNextToken();  // eat )
  return expr;
}

/// identifierexpr
///   ::= identifier
///   ::= identifier ( expression, expression, ..., expression )
std::unique_ptr<ExprAST> ParseIdentifierExpr() {
  std::string id = g_identifier_str;
  GetNextToken();
  if (g_current_token != '(') {
    return std::make_unique<VariableExprAST>(id);
  } else {
    GetNextToken();  // eat (
    std::vector<std::unique_ptr<ExprAST>> args;
    while (g_current_token != ')') {
      args.push_back(ParseExpression());
      if (g_current_token == ')') {
        break;
      } else {
        GetNextToken();  // eat ,
      }
    }
    GetNextToken();  // eat )
    return std::make_unique<CallExprAST>(id, std::move(args));
  }
}

上面代码中的 ParseExpression 与 ParseParenExpr 等存在循环依赖,这里按照其名字理解意思即可,具体实现在后面。我们将 NumberExpr、ParenExpr、IdentifierExpr 视为 PrimaryExpr, 封装 ParsePrimary 方便后续调用:

/// primary
///   ::= identifierexpr
///   ::= numberexpr
///   ::= parenexpr
std::unique_ptr<ExprAST> ParsePrimary() {
  switch (g_current_token) {
    case TOKEN_IDENTIFIER: return ParseIdentifierExpr();
    case TOKEN_NUMBER: return ParseNumberExpr();
    case '(': return ParseParenExpr();
    default: return nullptr;
  }
}

接下来我们考虑如何处理二元操作符,为了方便,Kaleidoscope 只支持 4 种二元操作符,优先级为:

'<' < '+' = '-' < '*'

即'<'的优先级最低,而'*'的优先级最高,在代码中实现为:

// 定义优先级
const std::map<char, int> g_binop_precedence = {
    {'<', 10}, {'+', 20}, {'-', 20}, {'*', 40}};

// 获得当前Token的优先级
int GetTokenPrecedence() {
  auto it = g_binop_precedence.find(g_current_token);
  if (it != g_binop_precedence.end()) {
    return it->second;
  } else {
    return -1;
  }
}

对于带优先级的二元操作符的解析,我们会将其分成多个片段。比如一个表达式:

a + b + (c + d) * e * f + g

首先解析 a, 然后处理多个二元组:

[+, b], [+, (c+d)], [*, e], [*, f], [+, g]

即,复杂表达式可以抽象为一个 PrimaryExpr 跟着多个[binop, PrimaryExpr]二元组,注意由于圆括号属于 PrimaryExpr, 所以这里不需要考虑怎么特殊处理(c+d),因为会被 ParsePrimary 自动处理。

// parse
//   lhs [binop primary] [binop primary] ...
// 如遇到优先级小于min_precedence的操作符,则停止
std::unique_ptr<ExprAST> ParseBinOpRhs(int min_precedence,
                                       std::unique_ptr<ExprAST> lhs) {
  while (true) {
    int current_precedence = GetTokenPrecedence();
    if (current_precedence < min_precedence) {
      // 如果当前token不是二元操作符,current_precedence为-1, 结束任务
      // 如果遇到优先级更低的操作符,也结束任务
      return lhs;
    }
    int binop = g_current_token;
    GetNextToken();  // eat binop
    auto rhs = ParsePrimary();
    // 现在我们有两种可能的解析方式
    //    * (lhs binop rhs) binop unparsed
    //    * lhs binop (rhs binop unparsed)
    int next_precedence = GetTokenPrecedence();
    if (current_precedence < next_precedence) {
      // 将高于current_precedence的右边的操作符处理掉返回
      rhs = ParseBinOpRhs(current_precedence + 1, std::move(rhs));
    }
    lhs =
        std::make_unique<BinaryExprAST>(binop, std::move(lhs), std::move(rhs));
    // 继续循环
  }
}

// expression
//   ::= primary [binop primary] [binop primary] ...
std::unique_ptr<ExprAST> ParseExpression() {
  auto lhs = ParsePrimary();
  return ParseBinOpRhs(0, std::move(lhs));
}

最复杂的部分完成后,按部就班把 function 写完:

// prototype
//   ::= id ( id id ... id)
std::unique_ptr<PrototypeAST> ParsePrototype() {
  std::string function_name = g_identifier_str;
  GetNextToken();
  std::vector<std::string> arg_names;
  while (GetNextToken() == TOKEN_IDENTIFIER) {
    arg_names.push_back(g_identifier_str);
  }
  GetNextToken();  // eat )
  return std::make_unique<PrototypeAST>(function_name, std::move(arg_names));
}

// definition ::= def prototype expression
std::unique_ptr<FunctionAST> ParseDefinition() {
  GetNextToken();  // eat def
  auto proto = ParsePrototype();
  auto expr = ParseExpression();
  return std::make_unique<FunctionAST>(std::move(proto), std::move(expr));
}

// external ::= extern prototype
std::unique_ptr<PrototypeAST> ParseExtern() {
  GetNextToken();  // eat extern
  return ParsePrototype();
}

最后,我们为顶层的代码实现匿名 function:

// toplevelexpr ::= expression
std::unique_ptr<FunctionAST> ParseTopLevelExpr() {
  auto expr = ParseExpression();
  auto proto = std::make_unique<PrototypeAST>("", std::vector<std::string>());
  return std::make_unique<FunctionAST>(std::move(proto), std::move(expr));
}

顶层代码的意思是放在全局而不放在 function 内定义的一些执行语句比如变量赋值,函数调用等。编写一个 main 函数:

int main() {
  GetNextToken();
  while (true) {
    switch (g_current_token) {
      case TOKEN_EOF: return 0;
      case TOKEN_DEF: {
        ParseDefinition();
        std::cout << "parsed a function definition" << std::endl;
        break;
      }
      case TOKEN_EXTERN: {
        ParseExtern();
        std::cout << "parsed a extern" << std::endl;
        break;
      }
      default: {
        ParseTopLevelExpr();
        std::cout << "parsed a top level expr" << std::endl;
        break;
      }
    }
  }
  return 0;
}

编译:

clang++ main.cpp `llvm-config --cxxflags --ldflags --libs`

输入如下代码进行测试:

def foo(x y)
    x + foo(y, 4)

def foo(x y)
    x + y

y

extern sin(a)

得到输出:

parsed a function definition
parsed a function definition
parsed a top level expr
parsed a extern

至此成功将 Lexer 输出的 tokens 转为 AST。

4. Code Generation to LLVM IR

终于开始 codegen 了,首先我们 include 一些 LLVM 头文件,定义一些全局变量:

#include "llvm/ADT/APFloat.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/DerivedTypes.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Type.h"
#include "llvm/IR/Verifier.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/Target/TargetMachine.h"
#include "llvm/Transforms/InstCombine/InstCombine.h"
#include "llvm/Transforms/Scalar.h"
#include "llvm/Transforms/Scalar/GVN.h"

// 记录了LLVM的核心数据结构,比如类型和常量表,不过我们不太需要关心它的内部
llvm::LLVMContext g_llvm_context;
// 用于创建LLVM指令
llvm::IRBuilder<> g_ir_builder(g_llvm_context);
// 用于管理函数和全局变量,可以粗浅地理解为类c++的编译单元(单个cpp文件)
llvm::Module g_module("my cool jit", g_llvm_context);
// 用于记录函数的变量参数
std::map<std::string, llvm::Value*> g_named_values;

然后给每个 AST Class 增加一个 CodeGen 接口:

// 所有 `表达式` 节点的基类
class ExprAST {
 public:
  virtual ~ExprAST() {}
  virtual llvm::Value* CodeGen() = 0;
};

// 字面值表达式
class NumberExprAST : public ExprAST {
 public:
  NumberExprAST(double val) : val_(val) {}
  llvm::Value* CodeGen() override;

 private:
  double val_;
};

首先实现 NumberExprAST 的 CodeGen:

llvm::Value* NumberExprAST::CodeGen() {
  return llvm::ConstantFP::get(g_llvm_context, llvm::APFloat(val_));
}

由于 Kaleidoscope 只有一种数据类型 FP64, 所以直接调用 ConstantFP 传入即可,APFloat 是 llvm 内部的数据结构,用于存储 Arbitrary Precision Float.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值