如何不借助LLVM生成工具,从抽象语法树生成LLVM IR?
从AST生成LLVM IR:告别字符串拼接,拥抱LLVM IR Builder API
我完全懂你的感受——用填空式字符串拼接生成LLVM IR确实能凑活,但写出来的代码不仅难维护,稍不注意就会因为格式错误或者IR语义问题踩坑,而且完全发挥不出LLVM的优势。其实LLVM本身就提供了一套更优雅、更可靠的方案:直接用IR Builder API在内存中构建IR结构,而不是手动拼字符串。
核心思路:用LLVM的IR Builder API直接构建IR
LLVM提供了一套C++(也有其他语言绑定)的IR Builder工具,它能帮你直接操作LLVM的内部IR对象,不需要手动拼接字符串。每个AST节点只需要实现一个codegen()方法,负责把自己转换成对应的LLVM IR值或结构。
举个简单的例子,假设你的AST里有个加法节点,包含左右两个子表达式节点,用Builder生成加法IR的代码大概是这样:
Value* AddExprAST::codegen() { // 先递归生成左右子表达式的IR值 Value* L = left->codegen(); Value* R = right->codegen(); // 如果子节点生成失败,直接返回空指针 if (!L || !R) return nullptr; // 用Builder创建加法指令,自动处理类型检查和临时变量命名 return Builder.CreateAdd(L, R, "addtmp"); }
这里的Builder是IRBuilder<>类型的实例,它会自动帮你处理IR的类型匹配、临时变量命名,生成的IR直接是LLVM的内部结构,最后只需要调用模块的打印方法就能输出成文本格式的IR。
为什么这比字符串拼接好?
- 类型安全:Builder会在运行期检查操作数的类型是否合法,比如你不能把整数和指针直接相加,Builder会直接报错;而字符串拼接只会生成无效IR,要到LLVM验证阶段才会暴露问题,排查起来麻烦得多。
- 可维护性:每个AST节点的
codegen()方法只关注自己对应的IR生成逻辑,逻辑清晰,修改起来方便。比如要调整加法的IR生成逻辑,只需要改AddExprAST::codegen(),不用在一堆字符串片段里找对应的拼接代码。 - 无缝对接优化:直接构建的IR可以直接传给LLVM的优化器(比如
PassManager)做优化,而字符串拼接出来的IR还需要先解析成LLVM内部结构,多了一步转换成本。
上手步骤拆解
- 搭建LLVM基础环境:先初始化LLVM的核心组件,包括上下文、模块和Builder:
static LLVMContext TheContext; static std::unique_ptr<Module> TheModule; static std::unique_ptr<IRBuilder<>> Builder; // 初始化模块和Builder TheModule = std::make_unique<Module>("my_custom_lang", TheContext); Builder = std::make_unique<IRBuilder<>>(TheContext); - 给每个AST节点添加
codegen方法:让所有AST类继承自一个基类(比如ExprAST),每个子类实现自己的codegen()方法,返回对应的LLVMValue*(值类型)或Function*(函数定义)。
比如数字节点的实现:Value* NumberExprAST::codegen() { return ConstantFP::get(TheContext, APFloat(Val)); } - 处理函数定义与调用:对于函数定义,需要先创建函数类型,再生成函数对象,最后用Builder在函数内部构建基本块和指令:
函数体的生成则需要先创建基本块,把Builder移动到该块内,再递归生成函数内部的IR。Function* PrototypeAST::codegen() { // 构建函数类型:比如接收两个double,返回double std::vector<Type*> Doubles(Args.size(), Type::getDoubleTy(TheContext)); FunctionType* FT = FunctionType::get(Type::getDoubleTy(TheContext), Doubles, false); // 创建函数并添加到模块中 Function* F = Function::Create(FT, Function::ExternalLinkage, Name, TheModule.get()); // 给参数设置名字,方便调试 unsigned Idx = 0; for (auto& Arg : F->args()) { Arg.setName(Args[Idx++]); } return F; }
避坑提示
- 不要忽略错误处理:每个
codegen()方法都要检查子节点的生成结果,如果返回nullptr表示出错,要及时向上返回,避免生成无效IR。 - 吃透LLVM核心概念:先搞懂基本块(BasicBlock)、值(Value)、类型(Type)这些核心概念,才能正确使用Builder。其实Kaleidoscope教程里的codegen部分就是用的这套API,只是结合了官方工具链,你可以把那部分代码抽出来单独使用。
- 用LLVM验证工具排查问题:生成IR后,可以调用
verifyFunction(F)或verifyModule(*TheModule)检查IR的合法性,快速定位问题。
内容的提问来源于stack exchange,提问作者user1762507
相关产品推荐
相关产品推荐

