[2] Using LLVM to implement a simple language

Posted by a2bardeals on Sun, 06 Mar 2022 14:53:35 +0100

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

Topics: llvm