5 generate LLVM IR
IR refers to intermediate expression, which is between high-level language and assembly language. Compared with high-level languages, it discards grammatical and semantic features, such as scope, object-oriented, etc; Compared with assembly language, there are no hardware related details, such as target machine architecture, operating system, etc.
First include some LLVM header files and define global variables:
#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" // Record some global data, such as types and constants used in each module static std::unique_ptr<LLVMContext> g_llvm_context; // A file is a module // The module contains functions and global variables static std::unique_ptr<Module> g_llvm_module; // Instruction for creating LLVM static std::unique_ptr<IRBuilder<>> g_ir_builder; // Variable parameters used to record functions static std::map<std::string, Value *> g_named_values;
5.1 add Codegen()
Add Codegen() to each AST class to generate LLVM IR.
/// ExprAST - Base class for all expression nodes. class ExprAST { public: virtual ~ExprAST() {} virtual Value *Codegen() = 0; }; /// NumberExprAST - Expression class for numeric literals like "1.0". class NumberExprAST : public ExprAST { double val_; public: NumberExprAST(double Val) : val_(val) {} virtual Value *Codegen(); }; ...
LLVM IR can also be generated through visitor mode. This article is not an engineering best practice. It would be easier to add codegen(). Codegen() will return LLVM Value, which is used to represent SSA (Static Single Assignment) Value. A variable can only be defined once and then used multiple times for code optimization.
5.2 implementation of Codegen()
Codegen() implementing NumberExprAST:
Value *NumberExprAST::Codegen() { return ConstantFP::get(g_llvm_context, APFloat(val_)); }
In LLVM IR, all constants are unique and shared, so use get instead of new/create.
Codegen(), which implements VariableExprAST:
Value *VariableExprAST::Codegen() { // Look this variable up in the function. Value *v = g_named_values[name_]; if (!v) LogErrorV("Unknown variable name"); return v; }
Current G_ named_ Only the function parameters are saved in values. When generating the IR of the function, the IR of the parameters will be generated and stored in g_named_values, so this is just getting. Back g_named_values saves the variables and local variables in the loop.
Codegen() implementing BinaryExprAST:
Value *BinaryExprAST::Codegen() { Value *l = lhs_->Codegen(); Value *r = rhs_->Codegen(); if (!l || !r) return nullptr; switch (opcode_) { case '+': return Builder.CreateFAdd(l, r, "addtmp"); case '-': return Builder.CreateFSub(l, r, "subtmp"); case '*': return Builder.CreateFMul(l, r, "multmp"); case '<': l = Builder.CreateFCmpULT(l, r, "cmptmp"); // Convert bool 0/1 to double 0.0 or 1.0 return Builder.CreateUIToFP(l, Type::GetDoubleTypepe(*g_llvm_context), "booltmp"); default: return LogErrorV("invalid binary operator"); } }
Recursively generate the IR of the left expression and the IR of the right expression, and then calculate the result. Builder.CreateFAdd() is used to insert the IR instruction of addition, and the third parameter represents the name, making the generated IR easy to read.
The LLVM instruction requires strict requirements. For example, the data types of L and R of the addition instruction must be the same, and the result type must match the operand type.
The data in createfloat() needs to be converted to the type of kaidboosl() in createfloat(), so it needs to be converted to the type of kaidboosl().
Codegen() implementing CallExprAST:
Value *CallExprAST::Codegen() { // Look up the name in the global module table. // All functions are stored in the LLVM Module Function *callee = g_llvm_module->getFunction(callee_); if (!callee) return LogErrorV("Unknown function referenced"); // If argument mismatch error. if (callee->arg_size() != args.size()) return LogErrorV("Incorrect # arguments passed"); std::vector<Value *> args_v; for (unsigned i = 0, e = args_.size(); i != e; ++i) { args_v.push_back(args_[i]->Codegen()); if (!args_v.back()) return nullptr; } return g_ir_builder.CreateCall(callee, args_v, "calltmp"); }
Codegen() implementing PrototypeAST:
// Function * is returned instead of Value* Function *PrototypeAST::Codegen() { // Make the function type: double(double, double) etc. std::vector<Type*> doubles(args_.size(), Type::GetDoubleTypepe(*g_llvm_context)); // In LLVM, Types are unique, so get is used instead of new // false indicates that the number of parameters is fixed FunctionType *f_type = FunctionType::get(Type::GetDoubleTypepe(*g_llvm_context), doubles, false); // Generate IR // The second parameter indicates that the function may be defined outside the current module, or the function can be called by other modules // The fourth parameter indicates which module to insert Function *fun = Function::Create(f_type, Function::ExternalLinkage, name_, g_llvm_module.get()); // Set names for all arguments. // This step is not necessary and can make the generated IR more readable // It also allows subsequent codes to directly reference parameters through parameter names, avoiding traversing PrototypeAST unsigned idx = 0; for (auto &arg : fun->args()) arg.setName(args[Idx++]); return fun; }
Codegen(), which implements FunctionAST: the function body will be added
Function *FunctionAST::Codegen() { // First, check for an existing function from a previous 'extern' declaration. // There may be an extern declaration Function *function = g_llvm_module->getFunction(proto_->name()); if (!function) function = proto_->Codegen(); if (!function) return nullptr; if (!function->empty()) return (Function*)LogErrorV("Function cannot be redefined."); // Create a new basic block to start insertion into. // Create a basic block named entry, which will be inserted into function // llvm block is used to define the control flow graph, but we will not implement the control flow temporarily. Just create a separate block BasicBlock *bb = BasicBlock::Create(g_llvm_context, "entry", function); // Set the instruction insertion point, that is, the following instructions will be inserted into bb Builder.SetInsertPoint(bb); // Record the function arguments in the g_named_values map. // g_named_values will be accessed by VariableExprAST g_named_values.clear(); for (auto &arg : function->args()) g_named_values[arg.getName()] = &arg; // The IR of the calculation expression will be sent to the entry block and the calculation result will be returned if (Value *ret_val = body_->Codegen()) { // Finish off the function. // Create a return value from the calculation result g_ir_builder.CreateRet(ret_val); // Validate the generated code, checking for consistency. verifyFunction(*function); return function; } // Error reading body, remove function. function->eraseFromParent(); return nullptr; }
There is still a problem with this code. If FunctionAST::Codegen() finds an existing IR, it will not check its signature.
5.3 operation
ready> 4+5; Read top-level expression: define double @0() { entry: ret double 9.000000e+00 } ready> def foo(a b) a*a + 2*a*b + b*b; Read function definition: define double @foo(double %a, double %b) { entry: %multmp = fmul double %a, %a %multmp1 = fmul double 2.000000e+00, %a %multmp2 = fmul double %multmp1, %b %addtmp = fadd double %multmp, %multmp2 %multmp3 = fmul double %b, %b %addtmp4 = fadd double %addtmp, %multmp3 ret double %addtmp4 }
6 optimization
The IR generated by IRBuilder is as follows:
ready> def test(x) 1+2+x; Read function definition: define double @test(double %x) { entry: %addtmp = fadd double 3.000000e+00, %x ret double %addtmp }
As you can see, constant folding is performed automatically.
However, if you want to optimize the following IR, you also need to introduce pass in LLVM.
ready> def test(x) (1+2+x)*(x+(1+2)); ready> Read function definition: define double @test(double %x) { entry: %addtmp = fadd double 3.000000e+00, %x %addtmp1 = fadd double %x, 3.000000e+00 %multmp = fmul double %addtmp, %addtmp1 ret double %multmp }
You can choose whether to enable pass and adjust the order of pass.
LLVM provides both pass for the entire Module and pass for a single function.
In this article, each function is optimized separately to initialize the pass Manager:
static std::unique_ptr<legacy::FunctionPassManager> g_fpm; void InitializeModuleAndPassManager(void) { // Open a new module. g_llvm_module = std::make_unique<Module>("my cool jit", g_llvm_context); // Create a new pass manager attached to it. g_fpm = std::make_unique<legacy::FunctionPassManager>(g_llvm_module.get()); // Do simple "peephole" optimizations and bit-twiddling optzns. g_fpm->add(createInstructionCombiningPass()); // Reassociate expressions. g_fpm->add(createReassociatePass()); // Eliminate Common SubExpressions. g_fpm->add(createGVNPass()); // Simplify the control flow graph (deleting unreachable blocks, etc). g_fpm->add(createCFGSimplificationPass()); g_fpm->doInitialization(); }
Add the following code in FunctionAST::Codegen():
if (Value *ret_val = body_->Codegen()) { // Finish off the function. g_ir_builder.CreateRet(ret_val); // Validate the generated code, checking for consistency. verifyFunction(*function); // Optimize the function. g_fpm->run(*function); return function; }
Test and see the effect:
ready> def test(x) (1+2+x)*(x+(1+2)); ready> Read function definition: define double @test(double %x) { entry: %addtmp = fadd double %x, 3.000000e+00 %multmp = fmul double %addtmp, %addtmp ret double %multmp }
7 add JIT
AOT: compile into executable file before execution.
Just in time compilation (JIT): compile when you need to run a piece of code. Java, JavaScript and other languages improve performance through instant compilation
JIT principle: dynamically apply for memory, load object code into memory, and grant executable permission to memory.
Define the global variables of JIT and initialize the environment through the initializeinatetarget * function.
#include "KaleidoscopeJIT.h" static std::unique_ptr<KaleidoscopeJIT> g_jit; ... int main() { InitializeNativeTarget(); InitializeNativeTargetAsmPrinter(); InitializeNativeTargetAsmParser(); g_jit = std::make_unique<KaleidoscopeJIT>(); ... return 0; }
Among them, kaleidoscope jit H is from the source code of llvm LLVM Src / examples / kaleidoscope / include / kaleidoscope JIT Copied from H.
Set data layout for JIT:
void InitializeModuleAndPassManager(void) { // Open a new module. g_llvm_module = std::make_unique<Module>("my cool jit", g_llvm_context); g_llvm_module->setDataLayout(g_jit->getTargetMachine().createDataLayout()); // Create a new pass manager attached to it. g_fpm = std::make_unique<legacy::FunctionPassManager>(g_llvm_module.get()); ...
Modify the top-level parsing function:
static void HandleTopLevelExpression() { // Evaluate a top-level expression into an anonymous function. if (auto fn_ast = ParseTopLevelExpr()) { if (fn_ast->Codegen()) { // After the top-level function Codegen, add the module containing the top-level function to the JIT // addModule will trigger the Codegen of all functions in the module and return a handle // You can delete this module through handle later auto handle = g_jit->addModule(std::move(g_llvm_module)); // Once a module is added to the JIT, it cannot be modified // Therefore, we also open a new module by calling InitializeModuleAndPassManager () to save the subsequent code. InitializeModuleAndPassManager(); // Search for symbols of top-level functions by their names auto expr_symbol = g_jit->findSymbol("__anon_expr"); assert(expr_symbol && "Function not found"); // Get the symbol address and cast it into a function pointer (double (*) () (no parameter, and return a double) double (*fp)() = (double (*)())(intptr_t)expr_symbol.getAddress(); fprintf(stderr, "Evaluated to %f\n", fp()); // Delete the anonymous expression module from the JIT. g_jit->removeModule(handle); }
function:
ready> def testfunc(x y) x + y*2; Read function definition: define double @testfunc(double %x, double %y) { entry: %multmp = fmul double %y, 2.000000e+00 %addtmp = fadd double %multmp, %x ret double %addtmp } ready> testfunc(4, 10); Read top-level expression: define double @1() { entry: %calltmp = call double @testfunc(double 4.000000e+00, double 1.000000e+01) ret double %calltmp } Evaluated to 24.000000 ready> testfunc(5, 10); ready> LLVM ERROR: Program used external function 'testfunc' which could not be resolved!
You can see that there is a problem. All functions are in the same module. When the module is deleted and released, the function definition is also deleted.
The simplest solution is to put the top-level function and other function definitions in different modules, so that if the module where the top-level function is located is deleted, it will not affect other functions.
We can also go further and put each function in its own module, so that the function can be added to the JIT many times. When we look for function symbols in JIT, we will always return the newly defined:
ready> def foo(x) x + 1; Read function definition: define double @foo(double %x) { entry: %addtmp = fadd double %x, 1.000000e+00 ret double %addtmp } ready> foo(2); Evaluated to 3.000000 ready> def foo(x) x + 2; define double @foo(double %x) { entry: %addtmp = fadd double %x, 2.000000e+00 ret double %addtmp } ready> foo(2); Evaluated to 4.000000
Regenerate the function definition in each newly opened module:
static std::unique_ptr<KaleidoscopeJIT> g_jit; // Store the latest definition of the function static std::map<std::string, std::unique_ptr<PrototypeAST>> g_function_protos; ... Function *GetFunction(std::string Name) { // First, search the function definition in the module if (auto *f = g_llvm_module->getFunction(Name)) return f; // If not, according to g_ function_ New generation definition of PROTOS auto fn = g_function_protos.find(Name); if (fn != g_function_protos.end()) return fn->second->Codegen(); // If no existing prototype exists, return null. return nullptr; } ... Value *CallExprAST::Codegen() { // Look up the name in the global module table. // Replace G_ llvm_ module->getFunction() Function *callee = GetFunction(Callee); ... Function *FunctionAST::Codegen() { // Transfer ownership of the prototype to the g_function_protos map, but keep a // reference to it for use below. auto &proto = *proto_; // Update G first_ function_ PROTOS, and then call getFunction g_function_protos[proto_->name()] = std::move(proto_); Function *function = GetFunction(proto.name()); if (!function) return nullptr;
HandleDefinition and HandleExtern also need to be updated:
static void HandleDefinition() { if (auto fn_ast = ParseDefinition()) { if (auto *fn_ir = fn_ast->Codegen()) { fprintf(stderr, "Read function definition:"); fn_ir->print(errs()); fprintf(stderr, "\n"); // Add the newly defined function to the JIT g_jit->addModule(std::move(g_llvm_module)); // Open new module InitializeModuleAndPassManager(); } } else { // Skip token for error recovery. NextToken(); } } static void HandleExtern() { if (auto proto_ast = ParseExtern()) { if (auto *fn_ir = proto_ast->Codegen()) { fprintf(stderr, "Read extern: "); fn_ir->print(errs()); fprintf(stderr, "\n"); // Update g_function_protos g_function_protos[proto_ast->name()] = std::move(proto_ast); } } else { // Skip token for error recovery. NextToken(); } }
Run again:
ready> def foo(x) x + 1; ready> foo(2); Evaluated to 3.000000 ready> def foo(x) x + 2; ready> foo(2); Evaluated to 4.000000
8 control flow
8.1 support If/Then/Else
Support new token in lexer:
// control TOKEN_IF = -6, TOKEN_then_ = -7, TOKEN_else_ = -8,
Identify new token:
... if (g_identifier_str == "if") return TOKEN_IF; if (g_identifier_str == "then") return TOKEN_THEN; if (g_identifier_str == "else") return TOKEN_ELSE;
Add a new AST node:
/// IfExprAST - Expression class for if/then/else. class IfExprAST : public ExprAST { std::unique_ptr<ExprAST> cond_, then_, else_; public: IfExprAST(std::unique_ptr<ExprAST> cond, std::unique_ptr<ExprAST> then, std::unique_ptr<ExprAST> else) : cond_(std::move(cond)), then_(std::move(then)), else_(std::move(else)) {} Value *Codegen() override; };
Define a new parsing function:
/// ifexpr ::= 'if' expression 'then' expression 'else' expression static std::unique_ptr<ExprAST> ParseIfExpr() { NextToken(); // eat the if. // condition. auto cond = ParseExpression(); if (!cond) return nullptr; if (g_cur_token != TOKEN_THEN) return LogError("expected then"); NextToken(); // eat the then auto then_expr = ParseExpression(); if (!then_expr) return nullptr; if (g_cur_token != TOKEN_ELSE) return LogError("expected else"); NextToken(); auto else_expr = ParseExpression(); if (!else) return nullptr; return std::make_unique<IfExprAST>(std::move(cond), std::move(then_expr), std::move(else_expr)); }
Modify the parsing of the basic expression:
static std::unique_ptr<ExprAST> ParsePrimary() { switch (g_cur_token) { default: return LogError("unknown token when expecting an expression"); case TOKEN_IDENTIFIER: return ParseIdentifierExpr(); case TOKEN_NUMBER: return ParseNumberExpr(); case '(': return ParseParenExpr(); case TOKEN_IF: return ParseIfExpr(); } }
Generate IR:
Value *IfExprAST::Codegen() { Value *cond_v = cond_->Codegen(); if (!cond_v) return nullptr; // Convert condition to a bool by comparing non-equal to 0.0. // Create fcmp one instruction, cond_value = (cond_value != 0.0) cond_v = g_ir_builder.CreateFCmpONE( cond_v, ConstantFP::get(g_llvm_context, APFloat(0.0)), "ifcond"); // Gets the function to instrumentation Function *function = g_ir_builder.GetInsertBlock()->getParent(); // Create blocks for the then and else cases. Insert the 'then' block at the // end of the function. // Create three blocks and insert the then block into the function // The other two block s have not been inserted into the function, because they must be then first_ BB generate instruction BasicBlock *then_bb = BasicBlock::Create(*g_llvm_context, "then", function); BasicBlock *else_bb = BasicBlock::Create(*g_llvm_context, "else"); BasicBlock *merge_bb = BasicBlock::Create(*g_llvm_context, "ifcond"); // Create conditional jump instruction, cond_ Jump to then when V is 1_ BB, jump to else at 0_ bb Builder.CreateCondBr(cond_v, then_bb, else_bb); // ---------------------- Emit then value. // Move the instrumentation point to then_bb g_ir_builder.SetInsertPoint(then_bb); Value *then_v = then_->Codegen(); if (!then_v) return nullptr; // To end then_bb, created a to merge_ Unconditional jump instruction of BB g_ir_builder.CreateBr(merge_bb); // Codegen of 'Then' can change the current block, update ThenBB for the PHI. // Update ThenBB, because ThenBB may have changed the block to be instrumented by Builder // For example, there may be if/then/else inside, and we need to get the final result block then_bb = g_ir_builder.GetInsertBlock(); // ----------------------- Emit else block. // Add ElseBB to function function->getBasicBlockList().push_back(else_bb); g_ir_builder.SetInsertPoint(else_bb); Value *else_v = else_->Codegen(); if (!else_v) return nullptr; g_ir_builder.CreateBr(MergeBB); // Codegen of 'else_' can change the current block, update else_bb for the PHI. else_bb = g_ir_builder.GetInsertBlock(); // ------------------------ Emit merge block. function->getBasicBlockList().push_back(merge_bb); g_ir_builder.SetInsertPoint(merge_bb); // Create PHI instruction: floating point type, 2 candidate values PHINode *pn = Builder.CreatePHI(Type::GetDoubleTypepe(g_llvm_context), 2, "iftmp"); pn->addIncoming(then_v, then_bb); // If it's from then_bb jump, using then_v pn->addIncoming(else_v, else_bb); // If it's from else_bb jump, using else_v return pn; }
8.2 support for loop expression
Next, support for expressions:
extern putchard(char); def printstar(n) for i = 1, i < n, 1.0 in putchard(42); # ascii 42 = '*' # print 100 '*' characters printstar(100);
Extended lexer:
... in enum Token ... TOKEN_FOR = -9, TOKEN_IN = -10, ... in GetToken ... if (g_identifier_str == "for") return TOKEN_FOR; if (g_identifier_str == "in") return TOKEN_IN; return TOKEN_IDENTIFIER;
Extended AST:
/// ForExprAST - Expression class for for/in. class ForExprAST : public ExprAST { std::string var_name_; std::unique_ptr<ExprAST> start_, end_, step_, body_; public: ForExprAST(const std::string &var_name, std::unique_ptr<ExprAST> start, std::unique_ptr<ExprAST> end, std::unique_ptr<ExprAST> step, std::unique_ptr<ExprAST> body;) : var_name_(var_name), start_(std::move(start)), end_(std::move(end)), step_(std::move(step)), body_(std::move(body)) {} Value *Codegen() override; };
Extended Parser:
/// forexpr ::= 'for' identifier '=' expr ',' expr (',' expr)? 'in' expression static std::unique_ptr<ExprAST> ParseForExpr() { NextToken(); // eat the for. if (g_cur_token != TOKEN_IDENTIFIER) return LogError("expected identifier after for"); std::string identifier_name = g_identifier_str; NextToken(); // eat identifier. if (g_cur_token != '=') return LogError("expected '=' after for"); NextToken(); // eat '='. auto start = ParseExpression(); if (!start) return nullptr; if (g_cur_token != ',') return LogError("expected ',' after for start_ value"); NextToken(); auto end = ParseExpression(); if (!end) return nullptr; // The step value is optional. std::unique_ptr<ExprAST> step; if (g_cur_token == ',') { NextToken(); step = ParseExpression(); if (!step) return nullptr; } if (g_cur_token != TOKEN_IN) return LogError("expected 'in' after for"); NextToken(); // eat 'in'. auto body = ParseExpression(); if (!body) return nullptr; return std::make_unique<ForExprAST>(identifier_name, std::move(start), std::move(end), std::move(step), std::move(body)); }
Update the parsing of the basic expression again:
static std::unique_ptr<ExprAST> ParsePrimary() { switch (g_cur_token) { ... case TOKEN_FOR: return ParseForExpr(); } }
Generate IR:
// Output for-loop as: // var = alloca double // ... // start = startexpr // store start -> var // goto loop // loop: // ... // bodyexpr // ... // loopend: // step = stepexpr // endcond = endexpr // // curvar = load var // nextvar = curvar + step // store nextvar -> var // br endcond, loop, endloop // outloop: Value *ForExprAST::Codegen() { // Emit the start code first, without 'variable' in scope. Value *start_val = start_->Codegen(); if (!start_val) return nullptr; // Make the new basic block for the loop header, inserting after current // block. Function *function = g_ir_builder->GetInsertBlock()->getParent(); // Get current block BasicBlock *preheader_bb = g_ir_builder->GetInsertBlock(); // Add loop_bb BasicBlock *loop_bb = BasicBlock::Create(g_llvm_context, "loop", function); // Insert an explicit fall through from the current block to the LoopBB. // Add from current block to loop_ Jump command of BB g_ir_builder.CreateBr(loop_bb); // Start insertion in loop_bb. // Start in loop_ Add instruction in BB g_ir_builder.SetInsertPoint(loop_bb); // Start the PHI node with an entry for Start. // Added PHI instruction PHINode *variable = Builder.CreatePHI(Type::GetDoubleTypepe(g_llvm_context), 2, var_name_.c_str()) // If it's from preheader_ If BB jumps, start is taken_ Value of Val variable->addIncoming(start_val, preheader_bb); // Within the loop, the variable is defined equal to the PHI node. If it // shadows an existing variable, we have to restore it, so save it now. // Prevent variables in loop from overwriting existing variables with the same name Value *old_val = g_named_values[var_name_]; g_named_values[var_name_] = variable; // Emit the body of the loop. // This, like any other expr, can change the // current BB. Note that we ignore the value computed by the body, but don't // allow an error. // body may be used. It has just been updated to g_ named_ loop variable in values if (!body_->Codegen()) return nullptr; // Emit the step value. Value *step_val = nullptr; if (step_) { step_val = step_->Codegen(); if (!step_val) return nullptr; } else { // If not specified, use 1.0. step_val = ConstantFP::get(g_llvm_context, APFloat(1.0)); } // After generating the body, you need to calculate the variable value in the next cycle Value *next_var = g_ir_builder.CreateFAdd(variable, step_val, "nextvar"); // Compute the end condition. Value *end_cond = end_->Codegen(); if (!end_ond) return nullptr; // Convert condition to a bool by comparing non-equal to 0.0. end_cond = g_ir_builder.CreateFCmpONE( end_cond, ConstantFP::get(g_llvm_context, APFloat(0.0)), "loopcond"); // Create the "after loop" block and insert it. BasicBlock *loop_end_bb = g_ir_builder.GetInsertBlock(); BasicBlock *after_bb = BasicBlock::Create(g_llvm_context, "afterloop", function); // Insert the conditional branch into the end of loop_end_bb. // Create a conditional jump instruction to decide whether to exit the loop g_ir_builder.CreateCondBr(end_cond, loop_bb, after_bb); // Any new code will be inserted in after_bb. // Subsequent instructions are inserted into after_bb medium g_ir_builder.SetInsertPoint(after_bb); // Add a new entry to the PHI node for the backedge. // If it is a cycle again, take next_var value variable->addIncoming(next_var, loop_end_bb); // Restore the unshadowed variable. // Variables in loop can only be used within the scope of loop if (old_val) g_g_named_values[var_name_] = old_val; else g_named_values.erase(var_name_); // for expr always returns 0.0. return Constant::getNullValue(Type::GetDoubleTypepe(g_llvm_context)); }
It can be seen that the PHI instruction is also used in the loop. If it is the first loop, the loop variable takes the StartVal value; Otherwise, take NextVal.
Compile run:
# Compile clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core orcjit native` -O3 -o toy # Run ./toy
9 user defined operators
We will allow users to add operators that do not exist, such as:
# Define unary operator! def unary!(v) if v then 0 else 1; # Define binary operator >, with priority of 10 def binary> 10 (LHS RHS) RHS < LHS; # Binary "logical or", (note that it does not "short circuit") # Define binary operator | with priority of 5 def binary| 5 (LHS RHS) if LHS then 1 else if RHS then 1 else 0; # Define = with slightly lower precedence than relationals. # Define binary operator =, with priority of 9 def binary= 9 (LHS RHS) !(LHS < RHS | LHS > RHS);
9.1 custom binary operators
Extended Lexer:
enum Token { ... // operators TOKEN_BINARY = -11, TOKEN_UNARY = -12, }; ... static int GetToken() { ... if (g_identifier_str == "binary") return TOKEN_BINARY; if (g_identifier_str == "unary") return TOKEN_UNARY; return TOKEN_IDENTIFIER; }
The new binary operator will be regarded as a function, so there is no need to add an AST node, just expand the PrototypeAST node:
/// PrototypeAST - This class represents the "prototype" for a function, /// which captures its argument names as well as if it is an operator. class PrototypeAST { std::string name_; std::vector<std::string> args_; bool is_operator_; // Is it an operator unsigned precedence_; // Priority of binary operators public: PrototypeAST(const std::string &name, std::vector<std::string> args_, bool is_operator = false, unsigned prec = 0) : name_(name), args_(std::move(args)), is_operator_(is_operator), precedence_(prec) {} Function *Codegen(); const std::string &name() const { return name_; } bool IsUnaryOpcode() const { return is_operator_ && args_.size() == 1; } bool IsBinaryOpcode() const { return is_operator_ && args_.size() == 2; } char operator_name() const { assert(IsUnaryOpcode() || IsBinaryOpcode()); return name_[name_.size() - 1]; } unsigned binary_precedence() const { return precedence_; } };
Modify Parser:
/// prototype /// ::= id '(' id* ')' /// ::= binary LETTER number? (id, id) static std::unique_ptr<PrototypeAST> ParsePrototype() { std::string fn_name; unsigned kind = 0; // 0 = identifier, 1 = unary, 2 = binary. unsigned binary_precedence = 30; switch (g_cur_token) { default: return LogErrorP("Expected function name in prototype"); case TOKEN_IDENTIFIER: fn_name = g_identifier_str; kind = 0; NextToken(); break; // Resolve binary operator name and priority case TOKEN_BINARY: NextToken(); if (!isascii(g_cur_token)) return LogErrorP("Expected binary operator"); fn_name = "binary"; fn_name += (char)g_cur_token; kind = 2; NextToken(); // Read the precedence if present. if (g_cur_token == TOKEN_NUMBER) { if (g_num_val < 1 || g_num_val > 100) return LogErrorP("Invalid precedence: must be 1..100"); binary_precedence = (unsigned)g_num_val; NextToken(); } break; } if (g_cur_token != '(') return LogErrorP("Expected '(' in prototype"); std::vector<std::string> arg_names; while (NextToken() == TOKEN_IDENTIFIER) arg_names.push_back(g_identifier_str); if (g_cur_token != ')') return LogErrorP("Expected ')' in proto_type"); // success. NextToken(); // eat ')'. // Verify right number of names for operator. if (kind && arg_names.size() != kind) return LogErrorP("Invalid number of oprand_s for operator"); return std::make_unique<PrototypeAST>(fn_name, arg_names, kind != 0, binary_precedence); }
Generate IR:
Value *BinaryExprAST::Codegen() { Value *l = lhs_->Codegen(); Value *r = rhs_->Codegen(); if (!l || !r) return nullptr; switch (opcode_) { case '+': return g_ir_builder->CreateFAdd(l, r, "addtmp"); case '-': return g_ir_builder->CreateFSub(l, r, "subtmp"); case '*': return g_ir_builder->CreateFMul(l, r, "multmp"); case '<': l = g_ir_builder->CreateFCmpULT(l, r, "cmptmp"); // Convert bool 0/1 to double 0.0 or 1.0 return g_ir_builder->CreateUIToFP(l, Type::GetDoubleTypepe(*g_llvm_context), "booltmp"); default: break; } // If it wasn't a builtin binary operator, it must be a user defined one. Emit // a call to it. Function *f = GetFunction(std::string("binary") + opcode_); assert(f && "binary operator not found!"); Value *opcodes[] = {l, r}; return g_ir_builder->CreateCall(f, opcodes, "binopcode"); }
IR of top-level function:
Function *FunctionAST::Codegen() { // Transfer ownership of the prototype to the g_function_protos map, but keep a // reference to it for use below. auto &proto = *proto_; g_function_protos[proto_->name()] = std::move(proto_); Function *function = GetFunction(proto.name()); if (!function) return nullptr; // If this is an operator, install it. if (P.IsBinaryOp()) // Register priority to prepare for subsequent Codegen g_binopcode_precedence[proto.operator_name()] = proto.binary_precedence(); // Create a new basic block to start insertion into. BasicBlock *bb = BasicBlock::Create(*g_llvm_context, "entry", function); ...
9.2 user defined unary operators
Supporting custom binary operators only extends the existing framework, while supporting unary operators will be more challenging.
Add AST node:
/// UnaryExprAST - Expression class for a unary operator. class UnaryExprAST : public ExprAST { char opcode_; std::unique_ptr<ExprAST> operand_; public: UnaryExprAST(char opcode, std::unique_ptr<ExprAST> oprand) : opcode_(opcode), oprand_(std::move(oprand)) {} Value *Codegen() override; };
Add Parser:
/// unary /// ::= primary /// ::= '!' unary static std::unique_ptr<ExprAST> ParseUnary() { // If the current token is not an operator, it must be a primary expr. if (!isascii(g_cur_token) || g_cur_token == '(' || g_cur_token == ',') return ParsePrimary(); // If this is a unary operator, read it. int opcode = g_cur_token; NextToken(); if (auto operand = ParseUnary()) return std::make_unique<UnaryExprAST>(opcode, std::move(operand)); return nullptr; }
The above code first deals with unary operators, and then regards the rest as another unary operation expression. In this way, it can deal with the case of multiple unary operators, such as!! x.
Next, you need to find a way for ParseUnary to be called to. Instead of calling ParsePrimary, you can call ParseUnary:
/// binoprhs /// ::= ('+' unary)* static std::unique_ptr<ExprAST> ParseBinOpRHS(int min_expr_prec, std::unique_ptr<ExprAST> lhs) { ... // Parse the unary expression after the binary operator. auto rhs = ParseUnary(); if (!rhs) return nullptr; ... } /// expression /// ::= unary binoprhs static std::unique_ptr<ExprAST> ParseExpression() { auto lhs = ParseUnary(); if (!lhs) return nullptr; return ParseBinOpRHS(0, std::move(lhs)); }
Expand the parsing of Prototype:
/// prototype /// ::= id '(' id* ')' /// ::= binary LETTER number? (id, id) /// ::= unary LETTER (id) static std::unique_ptr<PrototypeAST> ParsePrototype() { std::string fn_name; unsigned kind = 0; // 0 = identifier, 1 = unary, 2 = binary. unsigned binary_precedence = 30; switch (g_cur_token) { default: return LogErrorP("Expected function name in prototype"); case TOKEN_IDENTIFIER: fn_name = g_identifier_str; kind = 0; NextToken(); break; case TOKEN_UNARY: NextToken(); if (!isascii(g_cur_token)) return LogErrorP("Expected unary operator"); fn_name = "unary"; fn_name += (char)g_cur_token; kind = 1; NextToken(); break; case TOKEN_BINARY: ...
Generate IR:
Value *UnaryExprAST::Codegen() { Value *oprand_v = oprand_->Codegen(); if (!oprand_v) return nullptr; Function *f = GetFunction(std::string("unary") + opcode_); if (!f) return LogErrorV("Unknown unary operator"); g_ir_builder->CreateCall(f, oprand_v, "unopcode"); }
10 variable variables
Next, the following two functions will be supported:
- Assign a value to a variable by an equal sign.
- Define a new variable.
For example, the following examples are supported:
# Define ':' for sequencing: as a low-precedence operator that ignores operands # and just returns the RHS. def binary : 1 (x y) y; # Recursive fib, we could do this before. def fib(x) if (x < 3) then 1 else fib(x-1)+fib(x-2); # Iterative fib. def fibi(x) var a = 1, b = 1, c in (for i = 3, i < x in c = a + b : a = b : b = c) : b; # Call it. fibi(10);
In order to change the variable, you need to change the variable application method and add a new operator.
10.1 modify existing variables
g_named_values stores the LLVM Value * of the corresponding symbol name. In order to support variables, it needs to be changed to the "memory address" of the symbol.
First, g_named_values is mapped to AllocaInst * instead of Value *:
static std::map<std::string, AllocaInst*> g_named_values;
You need a helper function to ensure that alloca is created in the entry block of the function:
/// CreateEntryBlockAlloca - Create an alloca instruction in the entry block of /// the function. This is used for mutable variables etc. static AllocaInst *CreateEntryBlockAlloca(Function *function, const std::string &var_name) { // Create IRBuilder and point to the first instruction of the entry block IRBuilder<> tmp_b(&function->getEntryBlock(), function->getEntryBlock().begin()); // Create alloca return tmp_b.CreateAlloca(Type::GetDoubleTypepe(g_llvm_context), 0, var_name.c_str()); }
In the new mode, variables are in the stack, so to use them, you need to load them from the stack:
Value *VariableExprAST::Codegen() { // Look this variable up in the function. Value *v = g_named_values[name_]; if (!v) return LogErrorV("Unknown variable name"); // Load the value. g_ir_builder->CreateLoad(Type::GetDoubleTypepe(*g_llvm_context), v, name_.c_str()); }
Next, you need to update where you define variables to use alloca. Start with ForExprAST::Codegen():
Function *function = Builder.GetInsertBlock()->getParent(); // Create an alloca for the variable in the entry block. AllocaInst *alloca = CreateEntryBlockAlloca(function, var_name_); // Emit the start code first, without 'variable' in scope. Value *start_val = start_->Codegen(); if (!start_val) return nullptr; // Store the value into the alloca. g_ir_builder->CreateStore(start_val, alloca); ... // Compute the end condition. Value *end_condition = end_->Codegen(); if (!end_condition) return nullptr; // Reload, increment, and restore the alloca. This handles the case where // the body of the loop mutates the variable. Value *cur_var = g_ir_builder->CreateLoad(Type::GetDoubleTypepe(*g_llvm_context), alloca, var_name_.c_str()); Value *next_var = g_ir_builder->CreateFAdd(cur_var, step_val, "nextvar"); g_ir_builder->CreateStore(next_var, alloca); ...
As you can see, after using variables, you no longer need to create PHI nodes, but access the required variables through load/store.
Support function parameter variables:
Function *FunctionAST::Codegen() { ... g_ir_builder->SetInsertPoint(bb); // Record the function arguments in the g_named_values map. g_named_values.clear(); for (auto &arg : function->args()) { // Create an alloca for this variable. AllocaInst *alloca = CreateEntryBlockAlloca(function, arg.name()); // Store the initial value into the alloca. g_ir_builder->CreateStore(&arg, alloca); // Add arguments to variable symbol table. g_named_values[std::string(arg.name())] = alloca; } if (Value *ret_val = body_->Codegen()) { ...
Create an alloca for each parameter, that is, apply for memory on the stack, save the parameter value, and finally update the alloca to g_named_values.
The performance of saving temporary variables in memory is relatively low. mem2reg optimization can be used to store variables in registers:
// Promote allocas to registers. g_fpm->add(createPromoteMemoryToRegisterPass()); // Do simple "peephole" optimizations and bit-twiddling optzns. g_fpm->add(createInstructionCombiningPass()); // Reassociate expressions. g_fpm->add(createReassociatePass()); ...
10.2 new assignment symbols
Set priority:
int main() { // Install standard binary operators. // 1 is lowest precedence. g_binopcode_precedence['='] = 2; g_binopcode_precedence['<'] = 10; g_binopcode_precedence['+'] = 20; g_binopcode_precedence['-'] = 20;
Generate IR:
Value *BinaryExprAST::Codegen() { // Special case '=' because we don't want to emit the LHS as an expression. if (opcode_ == '=') { // Assignment requires the LHS to be an identifier. // Lvalue must be a variable VariableExprAST *lhs = static_cast<VariableExprAST *>(lhs_.get()); if (!lhs) return LogErrorV("destination of '=' must be a variable"); // Codegen the RHS. // Generate right value Value *val = rhs_->Codegen(); if (!val) return nullptr; // Look up the name. Value *variable = g_named_values[lhs->name()]; if (!variable) return LogErrorV("Unknown variable name"); // assignment g_ir_builder->CreateStore(val, variable); return val; } ...
Because of the assignment symbols and variables, the following code can be run:
# Function to print a double. extern printd(x); # Define ':' for sequencing: as a low-precedence operator that ignores operands # and just returns the RHS. def binary : 1 (x y) y; def test(x) printd(x) : x = 4 : printd(x); test(123);
10.3 customize local variables
Add var/in. As before, extending Kaleidoscope generally requires extensions: Lexer, Parser, AST and generating IR.
Extended Lexer:
enum Token { ... // var definition TOKEN_VAR = -13 ... } ... static int GetToken() { ... if (g_identifier_str == "var") return TOKEN_VAR; return TOKEN_IDENTIFIER; ...
Define AST nodes:
/// VarExprAST - Expression class for var/in // You can define multiple variables at once class VarExprAST : public ExprAST { std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> var_names_; std::unique_ptr<ExprAST> body_; // What to do with variables public: VarExprAST( std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> var_names_, std::unique_ptr<ExprAST> body) : var_names_(std::move(var_names_)), body_(std::move(body)) {} Value *Codegen() override; };
Extend Parser.
Add to the basic expression first:
/// primary /// ::= identifierexpr /// ::= numberexpr /// ::= parenexpr /// ::= ifexpr /// ::= forexpr /// ::= varexpr static std::unique_ptr<ExprAST> ParsePrimary() { switch (g_cur_token) { ... case TOKEN_VAR: return ParseVarExpr(); } }
Then define ParseVarExpr:
/// varexpr ::= 'var' identifier ('=' expression)? // (',' identifier ('=' expression)?)* 'in' expression static std::unique_ptr<ExprAST> ParseVarExpr() { NextToken(); // eat the var. std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> var_names; // At least one variable name is required. if (g_cur_token != TOKEN_IDENTIFIER) return LogError("expected identifier after var"); // Resolve all variables while (1) { std::string name = g_identifier_str; NextToken(); // eat identifier. // Read the opcodetional initializer. std::unique_ptr<ExprAST> init = nullptr; if (g_cur_token == '=') { NextToken(); // eat the '='. init = ParseExpression(); if (!init) return nullptr; } var_names.push_back(std::make_pair(name, std::move(init))); // End of var list, exit loop. if (g_cur_token != ',') break; NextToken(); // eat the ','. if (g_cur_token != TOKEN_IDENTIFIER) return LogError("expected identifier list after var"); } // At this point, we have to have 'in'. if (g_cur_token != TOKEN_IN) return LogError("expected 'in' keyword after 'var'"); NextToken(); // eat 'in'. // What to do with variables auto body = ParseExpression(); if (!body) return nullptr; return std::make_unique<VarExprAST>(std::move(var_names), std::move(body)); }
Generate IR:
Value *VarExprAST::Codegen() { std::vector<AllocaInst *> old_bindings; Function *function = Builder.GetInsertBlock()->getParent(); // Register all variables and emit their initializer. // Initialize each variable and register it in the symbol table for (unsigned i = 0, e = var_names_.size(); i != e; ++i) { const std::string &var_name = var_names_[i].first; ExprAST *init = var_names[i].second.get(); // Emit the initializer before adding the variable to scope, this prevents // the initializer from referencing the variable itself, and permits stuff // like this: // var a = 1 in // var a = a in ... # refers to outer 'a'. Value *init_val; if (init) { init_val = init->Codegen(); if (!init_val) return nullptr; } else { // If not specified, use 0.0. init_val = ConstantFP::get(*g_llvm_context, APFloat(0.0)); } AllocaInst *alloca = CreateEntryBlockAlloca(function, var_name); g_ir_builder->CreateStore(init_val, alloca); // Remember the old variable binding so that we can restore the binding when // we unrecurse. old_bindings.push_back(g_named_values[var_name]); // Remember this binding. g_named_values[var_name] = alloca; } // Codegen the body, now that all vars are in scope. // body can access all custom variables Value *body_val = body_->Codegen(); if (!body_val) return nullptr; // Pop all our variables from scope. for (unsigned i = 0, e = var_names_.size(); i != e; ++i) g_named_values[var_names_[i].first] = old_bindings[i]; // Return the body computation. return body_val; }
The following examples are now supported:
extern printd(x) def foo(x) if x < 3 then 1 else foo(x - 1) + foo(x - 2) for i = 1, i < 10, 1.0 in printd(foo(i))
Compile run:
# Compile clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core orcjit native` -O3 -o toy # Run ./toy
11 compile and generate target file
LLVM supports cross platform compilation. You can use the string of "target triple" to specify the architecture in the form of < arch > < sub > - < vendor > - < sys > - < ABI >. For example, clang believes that our architecture is:
$ clang --version | grep Target Target: x86_64-unknown-linux-gnu
The current architecture of getsys:: does not need the hard coding of the current architecture of LLVM:
auto target_triple = sys::getDefaultTargetTriple();
The TargetMachine class provides a complete description of the machine and can help us configure specific features or specific CPUs. For example, we can see which features and CPUs are supported:
$ llvm-as < /dev/null | llc -march=x86 -mattr=help Available CPUs for this target: amdfam10 - Select the amdfam10 processor. athlon - Select the athlon processor. athlon-4 - Select the athlon-4 processor. ... Available features for this target: 16bit-mode - 16-bit mode (i8086). 32bit-mode - 32-bit mode (80386). 3dnow - Enable 3DNow! instructions. 3dnowa - Enable 3DNow! Athlon instructions. ...
For example, we can use a general-purpose CPU that does not contain any additional feature s:
auto cpu = "generic"; auto features = ""; TargetOptions opt; auto rm = Optional<Reloc::Model>(); auto target_machine = target->createTargetMachine(target_triple, cpu, features, opt, rm);
The complete code is as follows:
int main() { ... // Run the main "interpreter loop" now. MainLoop(); // Initialize the target registry etc. // Initialize all target s that generate IR // LLVM does not require us to link all target s. If we only use JIT, we can not assemble the printer InitializeAllTargetInfos(); InitializeAllTargets(); InitializeAllTargetMCs(); InitializeAllAsmParsers(); InitializeAllAsmPrinters(); // Returns the architecture of the current machine auto target_triple = sys::getDefaultTargetTriple(); g_llvm_module->setTargetTriple(target_triple); // Get Target std::string error; auto target = TargetRegistry::lookupTarget(target_triple, error); // Print an error and exit if we couldn't find the requested target. // This generally occurs if we've forgotten to initialise the // TargetRegistry or we have a bogus target triple. if (!target) { errs() << error; return 1; } // Use a general-purpose CPU that does not contain any additional feature s auto cpu = "generic"; auto features = ""; TargetOptions opt; auto rm = Optional<Reloc::Model>(); // The TargetMachine class provides a complete description of the machine auto target_machine = target->createTargetMachine(target_triple, cpu, features, opt, rm); // Configuring the Module is not a necessary step, but it is helpful for optimization g_llvm_module->setDataLayout(target_machine->createDataLayout()); g_llvm_module->setTargetTriple(target_triple); // Target file auto filename = "output.o"; std::error_code err_code; raw_fd_ostream dest(filename, err_code, sys::fs::OF_None); if (err_code) { errs() << "Could not open file: " << err_code.message(); return 1; } // Define a pass to generate a target legacy::PassManager pass; auto file_type = CGFT_ObjectFile; if (target_machine->addPassesToEmitFile(pass, dest, nullptr, file_type)) { errs() << "TheTargetMachine can't emit a file of this type"; return 1; } // Run pass pass.run(*g_llvm_module); dest.flush(); outs() << "Wrote " << filename << "\n"; return 0; }
Compile run:
$ clang++ -g -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs all` -o toy $ ./toy ready> def average(x y) (x + y) * 0.5; ^D Wrote output.o
12 add Debug information
12.1 early compilation mode
We can't debug in JIT mode. Instead, we need to compile in advance (AOT), so we need a source file.
Optimization will combine multiple instructions into one, and may also remove some variables, so in order to debug, let's remove the optimization first.
First, we set the anonymous function containing the top-level statement to "main":
auto proto = std::make_unique<PrototypeAST>("main", std::vector<std::string>());
Then remove the command line code:
@@ -1129,7 +1129,6 @@ static void HandleTopLevelExpression() { /// top ::= definition | external | expression | ';' static void MainLoop() { while (1) { - fprintf(stderr, "ready> "); switch (g_cur_token) { case TOKEN_EOF: return; @@ -1184,7 +1183,6 @@ int main() { g_binop_precedence['*'] = 40; // highest. // Prime the first token. - fprintf(stderr, "ready> "); NextToken();
Finally, turn off all optimizations and jits:
static void HandleTopLevelExpression() { // Evaluate a top-level expression into an anonymous function. if (auto fn_ast = ParseTopLevelExpr()) { if (!fn_ast->codegen()) { fprintf(stderr, "Error generating code for top level expr"); } } else { // Skip token for error recovery. NextToken(); } }
Through the above changes, we can compile Kaleidoscope into an executable program a.out/a.exe through the following command:
Kaleidoscope-Ch9 < fib.ks | & clang -x ir -
12.2 DWARF settings
Source level debugging requires formatted data so that the debugger can convert binaries and machine code back to source code. In LLVM, the DWARF format is usually used, which is a compact encoding representing type, source location and variable location.
Similar to IRBuilder, DIBuilder can build debug metadata for LLVM IR files. We will use DIBuilder to build descriptions at all IR levels.
static std::unique_ptr<DIBuilder> g_di_builder; struct DebugInfo { DICompileUnit *compile_unit_; // The compilation unit can be understood as a source file DIType *di_type_; DIType *GetDoubleTypepe(); } g_dbg_info; // Just consider the double type DIType *DebugInfo::GetDoubleType() { if (di_type_) return di_type_; di_type_ = g_di_builder->createBasicType("double", 64, dwarf::DW_ATE_float); return di_type_; }
When building a module in main:
g_di_builder = new DIBuilder(*g_llvm_module); g_dbg_info.compile_unit_ = g_di_builder->createCompileUnit( // Constant in C is used, // Because the debugger does not necessarily understand the calling convention or default ABI of the language it does not recognize, // Therefore, it is most accurate to follow C ABI in LLVM code generation. // This ensures that we can call functions from the debugger and let them execute. dwarf::DW_LANG_C, // Hard coded file name g_di_builder->createFile("fib.ks", "."), "Kaleidoscope Compiler", 0, "", 0);
Add at the end of main:
g_di_builder->finalize();
12.3 functions
Next, you need to add some function definitions to the debug information. Add a few lines of code to PrototypeAST::Codegen() to describe the context of the program. In this case, the definition of "file" and the function itself.
Context:
DIFile *unit = DBuilder->createFile(g_dbg_info.compile_unit_.getFilename(), g_dbg_info.compile_unit_.getDirectory());
Use source location 0 (because AST has no source location information) and build the function definition:
DIScope *f_context = unit; unsigned line_no = p.line(); unsigned scope_line = line_no; // Disabprogram contains all metadata of the function DISubprogram *sp = g_di_builder->createFunction( f_context, p.name(), StringRef(), unit, line_no, CreateFunctionType(function->arg_size(), unit), scope_line, DINode::Flagprototyped, DISubprogram::SPFlagDefinition); function->setSubprogram(sp);
12.4 source location
The most important thing in debug ging is the exact source location.
Add source location information in Lexer to track row and column numbers:
struct SourceLocation { int line_; int col_; }; static SourceLocation g_cur_loc; static SourceLocation g_lex_loc = {1, 0}; static int Advance() { int last_char = getchar(); if (last_char == '\n' || last_char == '\r') { g_lex_loc.line_++; g_lex_loc.col_ = 0; } else g_lex_loc.col_++; return last_char; }
Add source location information to AST:
class ExprAST { SourceLocation loc_; public: ExprAST(SourceLocation loc = g_cur_loc) : loc_(loc) {} virtual ~ExprAST() {} virtual Value *Codegen() = 0; int line() const { return loc_.line_; } int col() const { return loc_.col_; } virtual raw_ostream &Dump(raw_ostream &out, int ind) { return out << ':' << line() << ':' << col() << '\n'; } };
When creating AST, the source location information is transferred in:
lhs = std::make_unique<BinaryExprAST>(bin_loc, binopcode, std::move(lhs), std::move(rhs));
In order to ensure that each instruction can get the correct source location, you need to tell Builder every time you reach a new location. Use a helper function to realize this function:
void DebugInfo::EmitLocation(ExprAST *ast) { DIScope *scope; if (lexical_blocks_.empty()) scope = compile_unit_; else scope = lexical_blocks_.back(); g_ir_builder->SetCurrentDebugLocation(DILocation::get( scope->getContext(), ast->line(), ast->col(), scope)); }
This tells the main IRBuilder where we are and what scope we are in. A stack scope is created in DebugInfo:
std::vector<DIScope *> lexical_blocks_;
Whenever a code is generated for a function, the scope of the function is put on the stack:
// Push the current scope. g_dbg_info.lexical_blocks_.push_back(sp);
When the code generation is finished, the scope of the function is out of the stack:
// Pop off the lexical block for the function since we added it // unconditionally. g_dbg_info.lexical_blocks_.pop_back();
Whenever a code is generated for a new AST, the launch location is required:
g_dbg_info.EmitLocation(this);
12.5 variables
Now that we have the function, we need to be able to print out the variables in the scope.
// Record the function arguments in the g_named_values map. g_named_values.clear(); unsigned arg_idx = 0; for (auto &arg : function->args()) { // Create an alloca for this variable. AllocaInst *alloca = CreateEntryBlockAlloca(function, arg.name()); // Create a debug descriptor for the variable. // Give the variable a scope (sp), source location, type and parameter subscript DILocalVariable *des = g_di_builder->createParameterVariable( sp, arg.name(), ++arg_idx, unit, line_no, g_dbg_info.GetDoubleType(), true); // Tell IR that we get a variable in alloca (give the position of the variable) // And set the source location for the starting location of the scope on declare g_di_builder->insertDeclare(alloca, des, g_di_builder->createExpression(), DILocation::get(SP->getContext(), line_no, 0, sp), g_ir_builder->GetInsertBlock()); // Store the initial value into the alloca. g_ir_builder.CreateStore(&arg, alloca); // Add arguments to variable symbol table. g_named_values[arg.name()] = alloca; }
In FunctionST::Codegen, several lines are added to avoid generating line information for the function preamble, so that the debugger knows to skip these instructions when setting breakpoints:
// Unset the location for the prologue emission (leading instructions with no // location in a function are considered part of the prologue and the debugger // will run past them when breaking on a function) g_dbg_info.EmitLocation(nullptr);
When actually starting to generate code for the function body, the emission position:
g_dbg_info.EmitLocation(body_.get());
With these, we have enough debugging information to set breakpoints, print parameter variables and call functions in the function.
Compile run:
# Compile clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core orcjit native` -O3 -o toy # Run ./toy
reference resources
https://llvm.org/docs/tutorial/index.html
Palace literature The beauty principle of compilation