Integrating the LLJIT engine into the calculator – JIT Compilation-1
Firstly, let’s discuss how to set up the JIT engine in our interactive calculator. All of the implementation pertaining to the JIT engine exists within Calc.cpp, and this file has one main() loop for the execution of the program:
- We must include several header files, aside from the headers including our code generation, semantic analyzer, and parser implementation. The LLJIT.h header defines the LLJIT class and the core classes of the ORC API. Next, the InitLLVM.h header is needed for the basic initialization of the tool, and the TargetSelect.h header is needed for the initialization of the native target. Finally, we also include the C++ header to allow for user input into our calculator application:
include “CodeGen.h”
include “Parser.h”
include “Sema.h”
include “llvm/ExecutionEngine/Orc/LLJIT.h”
include “llvm/Support/InitLLVM.h”
include “llvm/Support/TargetSelect.h”
include
- Next, we add the llvm and llvm::orc namespaces to the current scope:
using namespace llvm;
using namespace llvm::orc;
- Many of the calls from our LLJIT instance that we will be creating return an error type, Error. The ExitOnError class allows us to discard Error values that are returned by the calls from the LLJIT instance while logging to stderr and exiting the application. We declare a global ExitOnError variable as follows:
ExitOnError ExitOnErr;
- Then, we add the main() function, which initializes the tool and the native target:
int main(int argc, const char **argv{
InitLLVM X(argc, argv);
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
InitializeNativeTargetAsmParser();
- We use the LLJITBuilder class to create an LLJIT instance, wrapped in the previously declared ExitOnErr variable in case an error occurs. A possible source of error would be that the platform does not yet support JIT compilation:
auto JIT = ExitOnErr(LLJITBuilder().create());
- Next, we declare our JITtedFunctions map that keeps track of the function definitions, as we have previously described:
StringMap JITtedFunctions;
- To facilitate an environment that waits for user input, we add a while() loop and allow the user to type in an expression, saving the line that the user typed within a string called calcExp: while (true) {
outs() << “JIT calc > “;
std::string calcExp;
std::getline(std::cin, calcExp);
- Afterward, the LLVM context class is initialized, along with a new LLVM module. The module’s data layout is also set accordingly, and we also declare a code generator, which will be used to generate IR for the function that the user has defined on the command line: std::unique_ptr Ctx = std::make_unique();
std::unique_ptr M = std::make_unique(“JIT calc.expr”, *Ctx);
M->setDataLayout(JIT->getDataLayout());
CodeGen CodeGenerator;
- We must interpret the line that was entered by the user to determine if the user is defining a new function or calling a previous function that they have defined with an argument. A Lexer class is defined while taking in the line of input that the user has given. We will see that there are two main cases that the lexer cares about: Lexer Lex(calcExp);
Token::TokenKind CalcTok = Lex.peek();
- The lexer can check the first token of the user input. If the user is defining a new function (represented by the def keyword, or the Token::KW_def token), then we parse it and check its semantics. If the parser or the semantic analyzer detects any issues with the user-defined function, errors will be emitted accordingly, and the calculator program will halt. If no errors are detected from either the parser or the semantic analyzer, this means we have a valid AST data structure, DefDecl: if (CalcTok == Token::KW_def) {
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;
}
Leave a Reply