diff --git a/.travis.yml b/.travis.yml index 03a0186c2d..758a9ff39a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,6 +62,7 @@ addons: - boost - flex - bison + - llvm - openmpi - python@3 update: true @@ -105,7 +106,11 @@ install: script: - echo "------- Build, Test and Install -------" - mkdir build && pushd build - - cmake .. -DPYTHON_EXECUTABLE=$(which python3) -DCMAKE_INSTALL_PREFIX=$HOME/nmodl + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then + cmake .. -DPYTHON_EXECUTABLE=$(which python3) -DCMAKE_INSTALL_PREFIX=$HOME/nmodl -DNMODL_ENABLE_LLVM=ON -DLLVM_DIR=`brew --prefix llvm`/lib/cmake/llvm; + else + cmake .. -DPYTHON_EXECUTABLE=$(which python3) -DCMAKE_INSTALL_PREFIX=$HOME/nmodl -DNMODL_ENABLE_LLVM=OFF; + fi - make -j 2; if [ $? -ne 0 ]; then make VERBOSE=1; @@ -121,11 +126,11 @@ after_success: # Scikit-build does not support build_ext --inplace in custom commands. # More info at: https://github.com/scikit-build/scikit-build/issues/489 - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - echo "------- Build Documentation -------"; - python3 setup.py build_ext --inplace docs -j 2 -G "Unix Makefiles" || travis_terminate 1; - cd $TRAVIS_BUILD_DIR/_skbuild/linux-x86_64-3.8/setuptools/sphinx; - rm -rf doctest doctrees && touch .nojekyll; - echo "" > index.html; + echo "------- Skip Build Documentation -------"; + #python3 setup.py build_ext --inplace docs -j 2 -G "Unix Makefiles" || travis_terminate 1; + #cd $TRAVIS_BUILD_DIR/_skbuild/linux-x86_64-3.8/setuptools/sphinx; + #rm -rf doctest doctrees && touch .nojekyll; + #echo "" > index.html; fi #============================================================================= diff --git a/CMakeLists.txt b/CMakeLists.txt index b79743b1e4..29055452d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,7 @@ find_python_module(yaml 3.12 REQUIRED) # ============================================================================= if(NMODL_ENABLE_LLVM) include(LLVMHelper) + include_directories(${LLVM_INCLUDE_DIRS}) add_definitions(-DNMODL_LLVM_BACKEND) endif() diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3a2618fcdd..0e7b9b70ed 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -113,8 +113,7 @@ jobs: - checkout: self submodules: True - script: | - brew install flex cmake python@3 - brew install bison llvm + brew install flex bison cmake python@3 llvm python3 -m pip install -U pip setuptools python3 -m pip install --user 'Jinja2>=2.9.3' 'PyYAML>=3.13' pytest 'sympy>=1.3,<1.6' displayName: 'Install Dependencies' @@ -132,6 +131,7 @@ jobs: displayName: 'Build and Run Tests' - job: 'manylinux_wheels' timeoutInMinutes: 45 + condition: eq(1,2) pool: vmImage: 'ubuntu-18.04' strategy: @@ -177,6 +177,7 @@ jobs: - template: ci/upload-wheels.yml - job: 'macos_wheels' timeoutInMinutes: 45 + condition: eq(1,2) pool: vmImage: 'macOS-10.15' strategy: diff --git a/cmake/LLVMHelper.cmake b/cmake/LLVMHelper.cmake index dbd29c92b6..982af48660 100644 --- a/cmake/LLVMHelper.cmake +++ b/cmake/LLVMHelper.cmake @@ -1,15 +1,17 @@ # ============================================================================= # LLVM/Clang needs to be linked with either libc++ or libstdc++ # ============================================================================= + +find_package(LLVM REQUIRED CONFIG) + +# include LLVM header and core library +llvm_map_components_to_libnames(LLVM_LIBS_TO_LINK core) +set(CMAKE_REQUIRED_INCLUDES ${LLVM_INCLUDE_DIRS}) +set(CMAKE_REQUIRED_LIBRARIES ${LLVM_LIBS_TO_LINK}) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NMODL_ENABLE_LLVM) - find_package(LLVM REQUIRED CONFIG) include(CheckCXXSourceCompiles) - # test by including LLVM header and core library - llvm_map_components_to_libnames(LLVM_LIBS_TO_LINK core) - set(CMAKE_REQUIRED_INCLUDES ${LLVM_INCLUDE_DIRS}) - set(CMAKE_REQUIRED_LIBRARIES ${LLVM_LIBS_TO_LINK}) - # simple code to test LLVM library linking set(CODE_TO_TEST " diff --git a/setup.py b/setup.py index 281192bb0e..0a622593ba 100644 --- a/setup.py +++ b/setup.py @@ -94,7 +94,7 @@ def _config_exe(exe_name): ] -cmake_args = ["-DPYTHON_EXECUTABLE=" + sys.executable, "-DNMODL_ENABLE_LLVM=OFF"] +cmake_args = ["-DPYTHON_EXECUTABLE=" + sys.executable, "-DNMODL_ENABLE_LLVM=OFF", "-DNMODL_ENABLE_PYTHON_BINDINGS=ON"] if "bdist_wheel" in sys.argv: cmake_args.append("-DLINK_AGAINST_PYTHON=FALSE") diff --git a/src/codegen/llvm/codegen_llvm_visitor.cpp b/src/codegen/llvm/codegen_llvm_visitor.cpp index 494d5fd1f3..b8b3778e86 100644 --- a/src/codegen/llvm/codegen_llvm_visitor.cpp +++ b/src/codegen/llvm/codegen_llvm_visitor.cpp @@ -10,46 +10,153 @@ #include "visitors/visitor_utils.hpp" #include "llvm/IR/BasicBlock.h" +#include "llvm/IR/Constants.h" #include "llvm/IR/Function.h" -#include "llvm/IR/IRBuilder.h" #include "llvm/IR/LLVMContext.h" -#include "llvm/IR/Module.h" +#include "llvm/IR/Type.h" +#include "llvm/IR/ValueSymbolTable.h" namespace nmodl { namespace codegen { -// LLVM code generator objects -using namespace llvm; -static std::unique_ptr TheContext; -static std::unique_ptr TheModule; -static std::unique_ptr> Builder; -static std::map NamedValues; +/****************************************************************************************/ +/* Overloaded visitor routines */ +/****************************************************************************************/ -void CodegenLLVMVisitor::visit_statement_block(const ast::StatementBlock& node) { - logger->info("CodegenLLVMVisitor : visiting statement block"); - node.visit_children(*this); - // TODO : code generation for new block scope +void CodegenLLVMVisitor::visit_binary_expression(const ast::BinaryExpression& node) { + const auto& op = node.get_op().get_value(); + + // Process rhs first, since lhs is handled differently for assignment and binary + // operators. + node.get_rhs()->accept(*this); + llvm::Value* rhs = values.back(); + values.pop_back(); + if (op == ast::BinaryOp::BOP_ASSIGN) { + auto var = dynamic_cast(node.get_lhs().get()); + if (!var) { + throw std::runtime_error("Error: only VarName assignment is currently supported.\n"); + } + llvm::Value* alloca = named_values[var->get_node_name()]; + builder.CreateStore(rhs, alloca); + return; + } + + node.get_lhs()->accept(*this); + llvm::Value* lhs = values.back(); + values.pop_back(); + llvm::Value* result; + + // \todo: Support other binary operators + switch (op) { +#define DISPATCH(binary_op, llvm_op) \ + case binary_op: \ + result = llvm_op(lhs, rhs); \ + values.push_back(result); \ + break; + + DISPATCH(ast::BinaryOp::BOP_ADDITION, builder.CreateFAdd); + DISPATCH(ast::BinaryOp::BOP_DIVISION, builder.CreateFDiv); + DISPATCH(ast::BinaryOp::BOP_MULTIPLICATION, builder.CreateFMul); + DISPATCH(ast::BinaryOp::BOP_SUBTRACTION, builder.CreateFSub); + +#undef DISPATCH + } } -void CodegenLLVMVisitor::visit_procedure_block(const ast::ProcedureBlock& node) { - logger->info("CodegenLLVMVisitor : visiting {} procedure", node.get_node_name()); - - // print position, nmodl and json form as - /* - logger->info("Location {} \n NMODL {} \n JSON : {} \n", - node.get_token()->position(), - to_nmodl(node), - to_json(node)); - */ - node.visit_children(*this); - // TODO : code generation for procedure block +void CodegenLLVMVisitor::visit_boolean(const ast::Boolean& node) { + const auto& constant = llvm::ConstantInt::get(llvm::Type::getInt1Ty(*context), + node.get_value()); + values.push_back(constant); +} + +void CodegenLLVMVisitor::visit_double(const ast::Double& node) { + const auto& constant = llvm::ConstantFP::get(llvm::Type::getDoubleTy(*context), + node.get_value()); + values.push_back(constant); +} + +void CodegenLLVMVisitor::visit_integer(const ast::Integer& node) { + const auto& constant = llvm::ConstantInt::get(llvm::Type::getInt32Ty(*context), + node.get_value()); + values.push_back(constant); +} + +void CodegenLLVMVisitor::visit_local_list_statement(const ast::LocalListStatement& node) { + for (const auto& variable: node.get_variables()) { + // LocalVar always stores a Name. + auto name = variable->get_node_name(); + llvm::Type* var_type = llvm::Type::getDoubleTy(*context); + llvm::Value* alloca = builder.CreateAlloca(var_type, /*ArraySize=*/nullptr, name); + named_values[name] = alloca; + } } void CodegenLLVMVisitor::visit_program(const ast::Program& node) { node.visit_children(*this); - result_code = "Hello World"; + // Keep this for easier development (maybe move to debug mode later). + std::cout << print_module(); +} + +void CodegenLLVMVisitor::visit_procedure_block(const ast::ProcedureBlock& node) { + const auto& name = node.get_node_name(); + const auto& parameters = node.get_parameters(); + + // The procedure parameters are doubles by default. + std::vector arg_types; + for (size_t i = 0, e = parameters.size(); i < e; ++i) + arg_types.push_back(llvm::Type::getDoubleTy(*context)); + llvm::Type* return_type = llvm::Type::getVoidTy(*context); + + llvm::Function* proc = + llvm::Function::Create(llvm::FunctionType::get(return_type, arg_types, /*isVarArg=*/false), + llvm::Function::ExternalLinkage, + name, + *module); + + llvm::BasicBlock* body = llvm::BasicBlock::Create(*context, /*Name=*/"", proc); + builder.SetInsertPoint(body); + + // First, allocate parameters on the stack and add them to the symbol table. + unsigned i = 0; + for (auto& arg: proc->args()) { + std::string arg_name = parameters[i++].get()->get_node_name(); + llvm::Value* alloca = builder.CreateAlloca(arg.getType(), /*ArraySize=*/nullptr, arg_name); + arg.setName(arg_name); + builder.CreateStore(&arg, alloca); + named_values[arg_name] = alloca; + } + + const auto& statements = node.get_statement_block()->get_statements(); + for (const auto& statement: statements) { + // \todo: Support other statement types. + if (statement->is_local_list_statement() || statement->is_expression_statement()) + statement->accept(*this); + } + + values.clear(); + // \todo: Add proper support for the symbol table. + named_values.clear(); +} + +void CodegenLLVMVisitor::visit_unary_expression(const ast::UnaryExpression& node) { + ast::UnaryOp op = node.get_op().get_value(); + node.get_expression()->accept(*this); + llvm::Value* value = values.back(); + values.pop_back(); + if (op == ast::UOP_NEGATION) { + llvm::Value* result = builder.CreateFNeg(value); + values.push_back(result); + } else { + // Support only `double` operators for now. + throw std::runtime_error("Error: unsupported unary operator\n"); + } +} + +void CodegenLLVMVisitor::visit_var_name(const ast::VarName& node) { + llvm::Value* var = builder.CreateLoad(named_values[node.get_node_name()]); + values.push_back(var); } } // namespace codegen diff --git a/src/codegen/llvm/codegen_llvm_visitor.hpp b/src/codegen/llvm/codegen_llvm_visitor.hpp index 5b0ad3a968..5a288d9836 100644 --- a/src/codegen/llvm/codegen_llvm_visitor.hpp +++ b/src/codegen/llvm/codegen_llvm_visitor.hpp @@ -21,6 +21,10 @@ #include "utils/logger.hpp" #include "visitors/ast_visitor.hpp" +#include "llvm/IR/IRBuilder.h" +#include "llvm/IR/LLVMContext.h" +#include "llvm/IR/Module.h" + namespace nmodl { namespace codegen { @@ -45,8 +49,18 @@ class CodegenLLVMVisitor: public visitor::ConstAstVisitor { // Output directory for code generation std::string output_dir; - // result string for demo - std::string result_code; + private: + std::unique_ptr context = std::make_unique(); + + std::unique_ptr module = std::make_unique(mod_filename, *context); + + llvm::IRBuilder<> builder; + + // Stack to hold visited values + std::vector values; + + // Mappings for named values for lookups + std::map named_values; public: /** @@ -57,15 +71,27 @@ class CodegenLLVMVisitor: public visitor::ConstAstVisitor { */ CodegenLLVMVisitor(const std::string& mod_filename, const std::string& output_dir) : mod_filename(mod_filename) - , output_dir(output_dir) {} + , output_dir(output_dir) + , builder(*context) {} - void visit_statement_block(const ast::StatementBlock& node) override; + // Visitors + void visit_binary_expression(const ast::BinaryExpression& node) override; + void visit_boolean(const ast::Boolean& node) override; + void visit_double(const ast::Double& node) override; + void visit_integer(const ast::Integer& node) override; + void visit_local_list_statement(const ast::LocalListStatement& node) override; void visit_procedure_block(const ast::ProcedureBlock& node) override; void visit_program(const ast::Program& node) override; + void visit_unary_expression(const ast::UnaryExpression& node) override; + void visit_var_name(const ast::VarName& node) override; - // demo method - std::string get_code() const { - return result_code; + // TODO: use custom printer here + std::string print_module() const { + std::string str; + llvm::raw_string_ostream os(str); + os << *module; + os.flush(); + return str; } }; diff --git a/test/integration/mod/procedure.mod b/test/integration/mod/procedure.mod index ebbc39f15a..4017b6a505 100644 --- a/test/integration/mod/procedure.mod +++ b/test/integration/mod/procedure.mod @@ -1,5 +1,10 @@ +NEURON { + SUFFIX procedure_test + THREADSAFE +} + PROCEDURE hello_world() { - print("Hello World") + printf("Hello World") } PROCEDURE simple_sum(x, y) { @@ -20,7 +25,7 @@ PROCEDURE loop_function(v) { LOCAL i i = 0 WHILE(i < 10) { - print("Hello World") + printf("Hello World") i = i + 1 } } diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 3ef32e39b5..02ab230ce0 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -94,9 +94,10 @@ target_link_libraries( ${NMODL_WRAPPER_LIBS}) if(NMODL_ENABLE_LLVM) - add_executable(testcodegen visitor/main.cpp codegen/llvm.cpp) + include_directories(${LLVM_INCLUDE_DIRS}) + add_executable(testllvm visitor/main.cpp codegen/llvm.cpp) target_link_libraries( - testcodegen + testllvm visitor symtab lexer @@ -106,7 +107,7 @@ if(NMODL_ENABLE_LLVM) llvm_codegen ${NMODL_WRAPPER_LIBS} ${LLVM_LIBS_TO_LINK}) - set(CODEGEN_TEST testcodegen) + set(CODEGEN_TEST testllvm) endif() # ============================================================================= diff --git a/test/unit/codegen/llvm.cpp b/test/unit/codegen/llvm.cpp index b6efe2f9ca..270ce97ec0 100644 --- a/test/unit/codegen/llvm.cpp +++ b/test/unit/codegen/llvm.cpp @@ -6,20 +6,21 @@ *************************************************************************/ #include +#include #include "ast/program.hpp" +#include "codegen/llvm/codegen_llvm_visitor.hpp" #include "parser/nmodl_driver.hpp" #include "visitors/checkparent_visitor.hpp" #include "visitors/inline_visitor.hpp" #include "visitors/symtab_visitor.hpp" -#include "codegen/llvm/codegen_llvm_visitor.hpp" using namespace nmodl; using namespace visitor; using nmodl::parser::NmodlDriver; //============================================================================= -// Sample LLVM codegen test +// Utility to get LLVM module as a string //============================================================================= std::string run_llvm_visitor(const std::string& text) { @@ -31,21 +32,184 @@ std::string run_llvm_visitor(const std::string& text) { codegen::CodegenLLVMVisitor llvm_visitor("unknown", "."); llvm_visitor.visit_program(*ast); - return llvm_visitor.get_code(); + return llvm_visitor.print_module(); } -SCENARIO("Running LLVM Codegen", "[visitor][llvm]") { - GIVEN("Simple procedure with hello world message") { +//============================================================================= +// BinaryExpression and Double +//============================================================================= + +SCENARIO("Binary expression", "[visitor][llvm]") { + GIVEN("Procedure with addition of its arguments") { + std::string nmodl_text = R"( + PROCEDURE add(a, b) { + LOCAL i + i = a + b + } + )"; + + THEN("variables are loaded and add instruction is created") { + std::string module_string = run_llvm_visitor(nmodl_text); + std::smatch m; + + // Check the values are loaded correctly and added + std::regex rhs(R"(%1 = load double, double\* %b)"); + std::regex lhs(R"(%2 = load double, double\* %a)"); + std::regex res(R"(%3 = fadd double %2, %1)"); + REQUIRE(std::regex_search(module_string, m, rhs)); + REQUIRE(std::regex_search(module_string, m, lhs)); + REQUIRE(std::regex_search(module_string, m, res)); + } + } + + GIVEN("Procedure with multiple binary operators") { std::string nmodl_text = R"( - PROCEDURE say_hello() { - print("Hello World") + PROCEDURE multiple(a, b) { + LOCAL i + i = (a - b) / (a + b) } )"; - THEN("Hello world message is printed") { - std::string expected = "Hello World"; - auto result = run_llvm_visitor(nmodl_text); - REQUIRE(result == expected); + THEN("variables are processed from rhs first") { + std::string module_string = run_llvm_visitor(nmodl_text); + std::smatch m; + + // Check rhs + std::regex rr(R"(%1 = load double, double\* %b)"); + std::regex rl(R"(%2 = load double, double\* %a)"); + std::regex x(R"(%3 = fadd double %2, %1)"); + REQUIRE(std::regex_search(module_string, m, rr)); + REQUIRE(std::regex_search(module_string, m, rl)); + REQUIRE(std::regex_search(module_string, m, x)); + + // Check lhs + std::regex lr(R"(%4 = load double, double\* %b)"); + std::regex ll(R"(%5 = load double, double\* %a)"); + std::regex y(R"(%6 = fsub double %5, %4)"); + REQUIRE(std::regex_search(module_string, m, lr)); + REQUIRE(std::regex_search(module_string, m, ll)); + REQUIRE(std::regex_search(module_string, m, y)); + + // Check result + std::regex res(R"(%7 = fdiv double %6, %3)"); + REQUIRE(std::regex_search(module_string, m, res)); } } -} \ No newline at end of file + + GIVEN("Procedure with assignment") { + std::string nmodl_text = R"( + PROCEDURE assignment() { + LOCAL i + i = 2 + } + )"; + + THEN("double constant is stored into i") { + std::string module_string = run_llvm_visitor(nmodl_text); + std::smatch m; + + // Check store immediate is created + std::regex allocation(R"(%i = alloca double)"); + std::regex assignment(R"(store double 2.0*e\+00, double\* %i)"); + REQUIRE(std::regex_search(module_string, m, allocation)); + REQUIRE(std::regex_search(module_string, m, assignment)); + } + } +} + +//============================================================================= +// LocalList and LocalVar +//============================================================================= + +SCENARIO("Local variable", "[visitor][llvm]") { + GIVEN("Procedure with some local variables") { + std::string nmodl_text = R"( + PROCEDURE local() { + LOCAL i, j + } + )"; + + THEN("local variables are allocated on the stack") { + std::string module_string = run_llvm_visitor(nmodl_text); + std::smatch m; + + // Check stack allocations for i and j + std::regex i(R"(%i = alloca double)"); + std::regex j(R"(%j = alloca double)"); + REQUIRE(std::regex_search(module_string, m, i)); + REQUIRE(std::regex_search(module_string, m, j)); + } + } +} + +//============================================================================= +// ProcedureBlock +//============================================================================= + +SCENARIO("Procedure", "[visitor][llvm]") { + GIVEN("Empty procedure with no arguments") { + std::string nmodl_text = R"( + PROCEDURE empty() {} + )"; + + THEN("empty void function is produced") { + std::string module_string = run_llvm_visitor(nmodl_text); + std::smatch m; + + // Check procedure has empty body + std::regex procedure(R"(define void @empty\(\) \{\n\})"); + REQUIRE(std::regex_search(module_string, m, procedure)); + } + } + + GIVEN("Empty procedure with arguments") { + std::string nmodl_text = R"( + PROCEDURE with_argument(x) {} + )"; + + THEN("void function is produced with arguments allocated on stack") { + std::string module_string = run_llvm_visitor(nmodl_text); + std::smatch m; + + // Check procedure signature + std::regex function_signature(R"(define void @with_argument\(double %x1\) \{)"); + REQUIRE(std::regex_search(module_string, m, function_signature)); + + // Check that procedure arguments are allocated on the local stack + std::regex alloca_instr(R"(%x = alloca double)"); + std::regex store_instr(R"(store double %x1, double\* %x)"); + REQUIRE(std::regex_search(module_string, m, alloca_instr)); + REQUIRE(std::regex_search(module_string, m, store_instr)); + } + } +} + +//============================================================================= +// UnaryExpression +//============================================================================= + +SCENARIO("Unary expression", "[visitor][llvm]") { + GIVEN("Procedure with negation") { + std::string nmodl_text = R"( + PROCEDURE negation(a) { + LOCAL i + i = -a + } + )"; + + THEN("fneg instruction is created") { + std::string module_string = run_llvm_visitor(nmodl_text); + std::smatch m; + + std::regex allocation(R"(%1 = load double, double\* %a)"); + REQUIRE(std::regex_search(module_string, m, allocation)); + + // llvm v9 and llvm v11 implementation for negation + std::regex negation_v9(R"(%2 = fsub double -0.000000e\+00, %1)"); + std::regex negation_v11(R"(fneg double %1)"); + bool result = std::regex_search(module_string, m, negation_v9) || + std::regex_search(module_string, m, negation_v11); + REQUIRE(result == true); + } + } +}