Integrating the LLJIT engine into the calculator – JIT Compilation-2

  1. We then can pass our newly constructed AST into our code generator to compile the IR for the function that the user has defined. The specifics of IR generation will be discussed afterward, but this function that compiles to the IR needs to be aware of the module and our JITtedFunctions map. After generating the IR, we can add this information to our LLJIT instance by calling addIRModule() and wrapping our module and context in a ThreadSafeModule class, to prevent these from being accessed by other concurrent threads: CodeGenerator.compileToIR(Tree, M.get(), JITtedFunctions);
    ExitOnErr(
    JIT->addIRModule(ThreadSafeModule(std::move(M), std::move(Ctx))));
  1. Instead, if the user is calling a function with parameters, which is represented by the Token::ident token, we also need to parse and semantically check if the user input is valid prior to converting the input into a valid AST. The parsing and checking here are slightly different compared to before, as it can include checks such as ensuring the number of parameters that the user has supplied to the function call matches the number of parameters that the function was originally defined with: } else if (CalcTok == Token::ident) {
    outs() << “Attempting to evaluate expression:\n”;
    Parser Parser(Lex);
    AST *Tree = Parser.parse();
    if (!Tree || Parser.hasError()) {
    llvm::errs() << “Syntax errors occured\n”;
    return 1;
    }
    Sema Semantic;
    if (Semantic.semantic(Tree, JITtedFunctions)) {
    llvm::errs() << “Semantic errors occured\n”;
    return 1;
    }
  1. Once a valid AST is constructed for a function call, FuncCallFromDef, we get the name of the function from the AST, and then the code generator prepares to generate the call to the function that was previously added to the LLJIT instance. What occurs under the cover is that the user-defined function is regenerated as an LLVM call within a separate function that will be created that does the actual evaluation of the original function. This step requires the AST, the module, the function call name, and our map of function definitions: llvm::StringRef FuncCallName = Tree->getFnName();
    CodeGenerator.prepareCalculationCallFunc(Tree, M.get(), FuncCallName, JITtedFunctions);
  1. After the code generator has completed its work to regenerate the original function and to create a separate evaluation function, we must add this information to the LLJIT instance. We create a ResourceTracker instance to track the memory that is allocated to the functions that have been added to LLJIT, as well as another ThreadSafeModule instance of the module and context. These two instances are then added to the JIT as an IR module: auto RT = JIT->getMainJITDylib().createResourceTracker();
    auto TSM = ThreadSafeModule(std::move(M), std::move(Ctx));
    ExitOnErr(JIT->addIRModule(RT, std::move(TSM)));
  1. The separate evaluation function is then queried for within our LLJIT instance through the lookup() method, by supplying the name of our evaluation function, calc_expr_func, into the function. If the query is successful, the address for the calc_expr_func function is cast to the appropriate type, which is a function that takes no arguments and returns a single integer. Once the function’s address is acquired, we call the function to generate the result of the user-defined function with the parameters they have supplied and then print the result to the console: auto CalcExprCall = ExitOnErr(JIT->lookup(“calc_expr_func”));
    int (*UserFnCall)() = CalcExprCall.toPtr();
    outs() << “User defined function evaluated to: ” << UserFnCall() << “\n”;
  1. After the function call is completed, the memory that was previously associated with our functions is then freed by ResourceTracker:

ExitOnErr(RT->remove());