LLVM中的设计规范
首先LLVM中的库和工具都是通过C++来编写的, 利用了很多的面向对象的特性, 每一个对象都是一个组件或者叫做一个接口模块。
LLVM中经常使用模版, 但是需要小心模版带来的性能消耗, 也就是控制好C++项目的编译时间, 因为C++项目中的典型模版滥用问题会造成比较长的编译时间, 一旦有可能, LLVM会使用模版特化来允许实现快速
尽量使用libLLVMSupport中实现的断点机制,注意调试编译器是非常困难的一件事情, 因为编译器的产物是另外一种程序, 因此可以尽早检测出错误的行为, 就不需要为了确定的程序是否正确而编写一个并不重要的复杂输出。
LLVM代码中会出现另外一种常见的做法是使用智能指针, C++采用析构函数加上delete关键字来进行内存管理, delete关键字对应的语义是, 判断指针是否为空, 在指针不为空的时候调用析构函数并释放之前的内存。
在LLVM中使用轻量级别的字符串引用
如果频繁使用const string&会导致
LLVM 的IR 和 tools
传统的编译器后端往往都是手写的比如C语言, 由于LLVM的出现我们就避免去造轮子, 直接利用LLVM的给我们提供的库去做, 我们可以去看下面这张图片。
我们首先需要做的就是将AST转化成为一个LLVM IR, 为了方便我们直接使用这个S-expression来做为我们的语法, 因为lexer/parser其实是完全没有必要的。
首先我们创建一个文件:
int main(){return 42}
clang++ -S -emit-llvm test.cpp使用这个命令去生成一个IRclang++ -o test test.ll根据IR生成一个test可执行文件lli这个工具是解释器, 我们可以使用lli test.ll
编译后的IR具有一些元数据, 这些元数据是给编译器看的, 就相当于那个汇编代码的伪指令
define dso_local i32 @main() {
ret i32 42
}
IR的所有函数基本上都是全局的, 除此之外LLVM还有一个llvm-as可以生成这个bitcode, 还有一个反汇编工具llvm-dis可以将bitcode转化成为IR的。
我们在我们的文件中使用了LLVM的头文件, 我们需要在编译的时候引入这些东西, 我们可以写一个脚本
clang++ -o eva-llvm `llvm-config --cxxflags --ldflags --system-libs --libs core` \
eva-llvm.cpp
LLVM 程序结构
关于上面这张图片可以使用下面这段代码来解释:
llvm::Function* fn
std::unique_ptr<llvm::LLVMContext> ctx;
std::unique_ptr<llvm::Module> module;
std::unique_ptr<llvm::IRBuilder<>> builder;
ctx = std::make_unique<llvm::LLVMContext>();
module = std::make_unique<llvm::Module>("EvaLLVM", *ctx);
builder = std::make_unique<llvm::IRBuilder<>>(*ctx);
需要一个main函数, main函数对应的IR是这样的:
define i32 @main(){
entry:
ret i32 42
}
对应的CPP代码是
//1.函数名称
//2.函数的类型: FunctionType::get来创建
fn = createFunction(
"main",
llvm::FunctionType::get(
/*返回值类型*/ builder->getInt32Ty(),
/*不接收参数*/ false));
//创建一个main函数
llvm::Function* createFunction(const std::string& fnName, llvm::FunctionType* fnType) {
//在符号表中查找函数
auto fn = module->getFunction(fnName);
//如果没有在符号表中查找到函数
if (fn == nullptr) {
//创建一个函数原型
fn = createFunctionProto(fnName, fnType);
}
//创建函数入口块
createFunctionBlock(fn);
return fn;
}
llvm::Function* createFunctionProto(const std::string& fnName, llvm::FunctionType* fnType) {
auto fn = llvm::Function::Create(
fnType,
llvm::Function::ExternalLinkage,
fnName,
*module);
verifyFunction(*fn);
return fn;
}
void createFunctionBlock(llvm::Function* fn) {
//传入块所属的作用域
auto entry = createBB("entry", fn);
//一旦我们创建一个块, 我们必须显示告诉IRbuilder
builder->SetInsertPoint(entry);
}
llvm::BasicBlock* createBB(std::string name, llvm::Function* fn = nullptr){
return llvm::BasicBlock::Create(*ctx, name, fn);
}
我们首先声明那个标准库中的函数:
auto bytePtrTy = builder->getInt8Ty()->getPointerTo();
//打印函数
module->getOrInsertFunction(
"printf",
llvm::FunctionType::get(
/*return type*/builder->getInt32Ty(),
/*format args*/bytePtrTy,
/*vararg*/ true
));
上面我们自己构造了一个函数, 但是现在我们想调用一个标准库的函数
//创建一个字符串指针
auto str = builder->CreateGlobalStringPtr("Hello, World");
//获取IR中的函数
auto printfFn = module->getFunction("printf");
//函数参数
std::vector<llvm::Value*> args{str};
//在IR中生成
builder->CreateCall(printfFn, args);
在LLVM中关于变量的那些事情:
首先我们创建一个全局的作用域
llvm::GlobalVariable* createGlobalVar(const std::string& name, llvm::Constant* init)
{
module->getOrInsertGlobal(name, init->getType());
//从模块中获得全局变量
auto variable = module->getNamedGlobal(name);
//设置IR的大小
variable->setAlignment(llvm::MaybeAlign(4));
variable->setConstant(false);
//设置IR的初始值
variable->setInitializer(init);
return variable;
}
但是只有一个全局作用域的时候, 后面的变量会覆盖前面的变量:
@VERSION = global i32 getelementptr inbounds
我们可以自已设置一个环境:
void setupGlobalEnviroment()
{
//创建global
std::map<std::string, llvm::Value*> globalObject{
{"Version", builder->getInt32(42)},
};
std::map<std::string, llvm::Value*> globalRec{};
for (auto& entry: gloablObject)
{
globalRec[entry.first] =
createGloablVar(entry.first, (llvm::Constant*)entry.second);
}
GlobalEnv = std::make_shared<Enviroment>(globalRec, nullptr);
}
LLVM中的活动记录
前面我们定义了一种数据结构Environment, 这种数据结构方便我们去存储我们的变量, 我们现在创建的东西是方便去管理我们的环境。
/*
局部变量的声明格式
(var a 1)
(var (name i32) 32)
*/
//首先是获取声明
auto varNameDecl = exp.list[1];
auto varName = extractVarName(varNameDecl);
//获取初始化的值
auto init = gen(exp.list[2], env);
//获取变量的类型
auto varTy = extractVarType(varNameDecl);
//将变量类型, 变量的名称, 变量的环境所绑定
auto varBinding = allocVar(varName, varTy, env);
return builder->CreateStore(init, varBinding);
然后就是函数的具体细节
//x -> x
//(x number) -> x
std::string extractVarName(const Exp& exp)
{
return exp.type == ExpType::LIST ? exp.list[0].string : exp.string;
}
//x -> i32
//(x number) -> number
llvm::Type* extractVarType(const Exp& exp)
{
return exp.type == ExpType::LIST ?
getTypeFromString(exp.list[1].string) : builder->getInt32Ty();
}
然后给这些局部变量分配地址:
llvm::Value* allocVar(const std::string& name, llvm::Type* type_, Env env)
{
varsBuilder->SetInsertPoint(&fn->getEntryBlock());
auto varAlloc = varsBuilder->CreateAlloca(type_, 0, name.c_str());
//add to the environment
env->define(name, varAlloc);
return varAlloc;
}
从生成的代码上去看, 是SSA的, 在编译器的设计中, 静态单赋值形式是中间表示IR的特性, 每个变量被赋值一次, 在原始的IR中, 已经存在的变量可被分割成为许多不同的版本。
%x = alloca i32, align 4
store i32 42, i32* %x, align 4
%x1 = alloca i32, align 4
下面我们将引入if块, 我们知道if语句根据条件传递到不同的块, 如果条件为真的话, 我们会传递到一个块, 如果条件为假的话我们会传递到另外一个块
llvm中的if语句
创建三个block
auto thenBlock = createBB("then", fn);
auto elseBlock = createBB("else");
//这个叫做结果的合并块
auto ifEndBlock = createBB("ifend");
创建两个分支,因为需要执行代码之后才知道执行哪一个分支, 所有我们需要两个都创建, 但是我们可以做优化
//condation branch
builder->CreateCondBr(cond, thenBlock, elseBlock);
//then branch
builder->SetInsertPoint(thenBlock);
auto thenRes = gen(exp.list[2], env);
builder->CreateBr(ifEndBlock);
//else branch
builder->SetInsertPoint(elseBlock);
auto elseRes = gen(exp.list[3], env);
builder->CreateBr(ifEndBlock);
对应到IR就是:
br i1 %tmpcmp, label %then label %else
同时由于LLVM是采用整个的静态单点赋值, 所以应该不会在同一个块中出现两个相同的变量, 所以你不可以使用下面这样的语句
var x;
if (isDigit(x)) {
x = 2;
//llvm会释放掉x
} else {
x = "hello";
//llvm会释放掉x
}
console.log(x);
我们会使用llvm中的phi指令:
auto phi = builder->CreatePHI(thenRes->getType(), 2, "tmpif");
phi->addIncoming(thenRes, thenBlock);
phi->addIncoming(elseRes, elseBlock);
如果碰到if语句的嵌套, 往往又会出现一些问题, 所以我们需要解决一下嵌套问题, 我们可以添加一个块列表