diff --git a/serene/src/CMakeLists.txt b/serene/src/CMakeLists.txt index 8595cb8..8f0c3f1 100644 --- a/serene/src/CMakeLists.txt +++ b/serene/src/CMakeLists.txt @@ -19,5 +19,4 @@ target_sources(serene PRIVATE commands/commands.cpp jit/jit.cpp - context.cpp ) diff --git a/serene/src/context.cpp b/serene/src/context.cpp deleted file mode 100644 index 841ed92..0000000 --- a/serene/src/context.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* -*- C++ -*- - * Serene Programming Language - * - * Copyright (c) 2019-2023 Sameer Rahmani - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 2. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include // for exit - -namespace serene { - -int SereneContext::getOptimizatioLevel() { - if (targetPhase <= CompilationPhase::NoOptimization) { - return 0; - } - - if (targetPhase == CompilationPhase::O1) { - return 1; - } - if (targetPhase == CompilationPhase::O2) { - return 2; - } - return 3; -} - -void terminate(SereneContext &ctx, int exitCode) { - (void)ctx; - // TODO: Since we are running in a single thread for now using exit is fine - // but we need to adjust and change it to a thread safe termination - // process later on. - // NOLINTNEXTLINE(concurrency-mt-unsafe) - std::exit(exitCode); -} - -std::unique_ptr makeSereneContext(Options opts) { - return std::make_unique(opts); -}; - -}; // namespace serene diff --git a/serene/src/context.h b/serene/src/context.h deleted file mode 100644 index 3955b1c..0000000 --- a/serene/src/context.h +++ /dev/null @@ -1,104 +0,0 @@ -/* -*- C++ -*- - * Serene Programming Language - * - * Copyright (c) 2019-2023 Sameer Rahmani - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 2. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef CONTEXT_H -#define CONTEXT_H - -#include "options.h" // for Options -#include <__fwd/string.h> // for string -#include <__memory/unique_ptr.h> // for make_unique, unique_ptr - -#include // for Twine -#include // for LLVMContext -#include // for getDefaultTargetTriple -#include // for Triple - -#include // for basic_string -#include // for vector - -namespace serene { -class SereneContext; - -/// This enum describes the different operational phases for the compiler -/// in order. Anything below `NoOptimization` is considered only for debugging -enum class CompilationPhase { - Parse, - Analysis, - SLIR, - MLIR, // Lowered slir to other dialects - LIR, // Lowered to the llvm ir dialect - IR, // Lowered to the LLVMIR itself - NoOptimization, - O1, - O2, - O3, -}; - -/// Terminates the serene compiler process in a thread safe manner -/// This function is only meant to be used in the compiler context -/// if you want to terminate the process in context of a serene program -/// via the JIT use an appropriate function in the `serene.core` ns. -void terminate(SereneContext &ctx, int exitCode); - -// Why SereneContext and not Context? We will be using LLVMContext -// and MLIRContext through out Serene, so it's better to follow -// the same convention -class SereneContext { - -public: - /// The set of options to change the compilers behaviors - Options options; - - const llvm::Triple triple; - - explicit SereneContext(Options &options) - : options(options), triple(llvm::sys::getDefaultTargetTriple()), - targetPhase(CompilationPhase::NoOptimization){}; - - /// Set the target compilation phase of the compiler. The compilation - /// phase dictates the behavior and the output type of the compiler. - void setOperationPhase(CompilationPhase phase); - - CompilationPhase getTargetPhase() { return targetPhase; }; - int getOptimizatioLevel(); - - static std::unique_ptr genLLVMContext() { - return std::make_unique(); - }; - - /// Setup the load path for namespace lookups - void setLoadPaths(std::vector &dirs) { loadPaths.swap(dirs); }; - - /// Return the load paths for namespaces - std::vector &getLoadPaths() { return loadPaths; }; - -private: - CompilationPhase targetPhase; - std::vector loadPaths; -}; - -/// Creates a new context object. Contexts are used through out the compilation -/// process to store the state. -/// -/// \p opts is an instance of \c Options that can be used to set options of -/// of the compiler. -std::unique_ptr makeSereneContext(Options opts = Options()); - -} // namespace serene - -#endif diff --git a/serene/src/jit/jit.cpp b/serene/src/jit/jit.cpp index caed305..5f88524 100644 --- a/serene/src/jit/jit.cpp +++ b/serene/src/jit/jit.cpp @@ -16,21 +16,22 @@ * along with this program. If not, see . */ -#include "jit/jit.h" - -#include "options.h" // for Options +#include #include // for error_code #include // for StringMapEntry #include // for iterator_facade_base #include // for JITEventListener #include // IWYU pragma: keep -#include // for Module -#include // for OpenFlags -#include // for ToolOutputFile +#include +#include +#include // for Module +#include // for OpenFlags +#include // for ToolOutputFile -#include // for assert -#include // for operator+, char_t... +#include // for assert +#include // for Options +#include // for operator+, char_t... namespace serene::jit { @@ -113,15 +114,168 @@ size_t JIT::getNumberOfJITDylibs(const llvm::StringRef &nsName) { return jitDylibs[nsName].size(); }; -JIT::JIT(llvm::orc::JITTargetMachineBuilder &&jtmb, Options &opts) - : isLazy(opts.JITLazy), - cache(opts.JITenableObjectCache ? new ObjectCache() : nullptr), - gdbListener(opts.JITenableGDBNotificationListener +JIT::JIT(llvm::orc::JITTargetMachineBuilder &&jtmb, + std::unique_ptr opts) + : + + options(std::move(opts)), + cache(options->JITenableObjectCache ? new ObjectCache() : nullptr), + gdbListener(options->JITenableGDBNotificationListener ? llvm::JITEventListener::createGDBRegistrationListener() : nullptr), - perfListener(opts.JITenablePerfNotificationListener + perfListener(options->JITenablePerfNotificationListener ? llvm::JITEventListener::createPerfJITEventListener() : nullptr), jtmb(jtmb){}; +void JIT::dumpToObjectFile(const llvm::StringRef &filename) { + cache->dumpToObjectFile(filename); +}; + +int JIT::getOptimizatioLevel() const { + if (options->compilationPhase <= CompilationPhase::NoOptimization) { + return 0; + } + + if (options->compilationPhase == CompilationPhase::O1) { + return 1; + } + if (options->compilationPhase == CompilationPhase::O2) { + return 2; + } + return 3; +} + +llvm::Error JIT::createCurrentProcessJD() { + + auto &es = WITH_ENGINE(auto &, getExecutionSession()); + auto *processJDPtr = es.getJITDylibByName(MAIN_PROCESS_JD_NAME); + + if (processJDPtr != nullptr) { + // We already created the JITDylib for the current process + return llvm::Error::success(); + } + + auto processJD = es.createJITDylib(MAIN_PROCESS_JD_NAME); + + if (!processJD) { + return processJD.takeError(); + } + + auto generator = + llvm::orc::DynamicLibrarySearchGenerator::GetForCurrentProcess( + WITH_ENGINE(const auto &, getDataLayout()).getGlobalPrefix()); + + if (!generator) { + return generator.takeError(); + } + + processJD->addGenerator(std::move(*generator)); + return llvm::Error::success(); +}; + +MaybeJIT JIT::make(llvm::orc::JITTargetMachineBuilder &&jtmb, + std::unique_ptr opts) { + auto dl = jtmb.getDefaultDataLayoutForTarget(); + if (!dl) { + return dl.takeError(); + } + + auto jitEngine = std::make_unique(std::move(jtmb), std::move(opts)); + // + // What might go wrong? + // in a repl env when we have to create new modules on top of each other + // having two different contex might be a problem, but i think since we + // use the first context to generate the IR and the second one to just + // run it. + std::unique_ptr ctx(new llvm::LLVMContext); + + // Callback to create the object layer with symbol resolution to current + // process and dynamically linked libraries. + auto objectLinkingLayerCreator = [&](llvm::orc::ExecutionSession &session, + const llvm::Triple &tt) { + (void)tt; + + auto objectLayer = + std::make_unique(session, []() { + return std::make_unique(); + }); + + // Register JIT event listeners if they are enabled. + if (jitEngine->gdbListener != nullptr) { + objectLayer->registerJITEventListener(*jitEngine->gdbListener); + } + if (jitEngine->perfListener != nullptr) { + objectLayer->registerJITEventListener(*jitEngine->perfListener); + } + + // COFF format binaries (Windows) need special handling to deal with + // exported symbol visibility. + // cf llvm/lib/ExecutionEngine/Orc/LLJIT.cpp + // LLJIT::createObjectLinkingLayer + if (jitEngine->hostTriple.isOSBinFormatCOFF()) { + objectLayer->setOverrideObjectFlagsWithResponsibilityFlags(true); + objectLayer->setAutoClaimResponsibilityForObjectSymbols(true); + } + + return objectLayer; + }; + + // Callback to inspect the cache and recompile on demand. + auto compileFunctionCreator = [&](llvm::orc::JITTargetMachineBuilder JTMB) + -> llvm::Expected< + std::unique_ptr> { + llvm::CodeGenOpt::Level jitCodeGenOptLevel = + static_cast(jitEngine->getOptimizatioLevel()); + + JTMB.setCodeGenOptLevel(jitCodeGenOptLevel); + + auto targetMachine = JTMB.createTargetMachine(); + if (!targetMachine) { + return targetMachine.takeError(); + } + + return std::make_unique( + std::move(*targetMachine), jitEngine->cache.get()); + }; + + auto compileNotifier = [&](llvm::orc::MaterializationResponsibility &r, + llvm::orc::ThreadSafeModule tsm) { + auto syms = r.getRequestedSymbols(); + tsm.withModuleDo([&](llvm::Module &m) { + HALLEY_LOG("Compiled " << syms + << " for the module: " << m.getModuleIdentifier()); + }); + }; + + if (jitEngine->options->JITLazy) { + // Setup a LLLazyJIT instance to the times that latency is important + // for example in a REPL. This way + auto jit = + cantFail(llvm::orc::LLLazyJITBuilder() + .setCompileFunctionCreator(compileFunctionCreator) + .setObjectLinkingLayerCreator(objectLinkingLayerCreator) + .create()); + jit->getIRCompileLayer().setNotifyCompiled(compileNotifier); + jitEngine->engine = std::move(jit); + + } else { + // Setup a LLJIT instance for the times that performance is important + // and we want to compile everything as soon as possible. For instance + // when we run the JIT in the compiler + auto jit = + cantFail(llvm::orc::LLJITBuilder() + .setCompileFunctionCreator(compileFunctionCreator) + .setObjectLinkingLayerCreator(objectLinkingLayerCreator) + .create()); + jit->getIRCompileLayer().setNotifyCompiled(compileNotifier); + jitEngine->engine = std::move(jit); + } + + if (auto err = jitEngine->createCurrentProcessJD()) { + return err; + } + + return MaybeJIT(std::move(jitEngine)); +}; } // namespace serene::jit diff --git a/serene/src/jit/jit.h b/serene/src/jit/jit.h index 663609c..824ac66 100644 --- a/serene/src/jit/jit.h +++ b/serene/src/jit/jit.h @@ -59,9 +59,19 @@ namespace serene { struct Options; } // namespace serene +#define MAIN_PROCESS_JD_NAME "*main*" #define HALLEY_LOG(...) \ DEBUG_WITH_TYPE("JIT", llvm::dbgs() << "[JIT]: " << __VA_ARGS__ << "\n"); +/// A simple macro that we need to use to call those member functions that are +/// shared between LLJIT and LLLAZYJIT. This macro supposed to be used +/// only within the JIT class itself. The first argument is the return type +/// of the member function and the second arg is the member function call. +/// The whole point of this macro is to unwrap the variant type and call +/// the shared member function on the unwraped value. +#define WITH_ENGINE(retType, fnCall) \ + std::visit([](auto &e) -> retType { return e->fnCall; }, engine) + namespace orc = llvm::orc; namespace serene::jit { @@ -91,7 +101,7 @@ private: }; class JIT { - const bool isLazy; + std::unique_ptr options; std::variant, std::unique_ptr> engine; @@ -103,6 +113,8 @@ class JIT { llvm::orc::JITTargetMachineBuilder jtmb; + std::vector loadPaths; + // We keep the jibDylibs for each name space in a mapping from the ns // name to a vector of jitdylibs, the last element is always the newest // jitDylib @@ -115,9 +127,30 @@ class JIT { void pushJITDylib(const llvm::StringRef &nsName, llvm::orc::JITDylib *l); size_t getNumberOfJITDylibs(const llvm::StringRef &nsName); + llvm::Error createCurrentProcessJD(); + public: - JIT(llvm::orc::JITTargetMachineBuilder &&jtmb, Options &opts); - static MaybeJIT make(llvm::orc::JITTargetMachineBuilder &&jtmb); + // We will use this triple to generate code that will endup in the binary + // for the target platform. If we're not cross compiling, `targetTriple` + // will be the same as `hostTriple`. + const llvm::Triple targetTriple; + + // This triple will be used in code generation for the host platform in + // complie time. For example any function that will be called during + // the compile time has to run on the host. So we need to generate + // appropriate code for the host. If the same function has to be part + // of the runtime, then we use `targetTriple` again to generate the code + // for the target platform. So, we might end up with two version of the + // same function + const llvm::Triple hostTriple; + + JIT(llvm::orc::JITTargetMachineBuilder &&jtmb, std::unique_ptr opts); + static MaybeJIT make(llvm::orc::JITTargetMachineBuilder &&jtmb, + std::unique_ptr opts); + + // Return an integer indicating the level of optimization that is currently + // set. 0 == No optimizaion -> it includes compling to IR and AST + int getOptimizatioLevel() const; /// Return a pointer to the most registered JITDylib of the given \p ns ////name @@ -137,6 +170,11 @@ public: llvm::Error loadModule(const llvm::StringRef &nsName, const llvm::StringRef &file); void dumpToObjectFile(const llvm::StringRef &filename); + + /// Setup the load path for namespace lookups + void setLoadPaths(std::vector &dirs) { loadPaths.swap(dirs); }; + /// Return the load paths for namespaces + llvm::ArrayRef getLoadPaths() { return loadPaths; }; }; MaybeJIT makeJIT(); diff --git a/serene/src/options.h b/serene/src/options.h index 7094d51..42af54a 100644 --- a/serene/src/options.h +++ b/serene/src/options.h @@ -20,6 +20,21 @@ #define OPTIONS_H namespace serene { +/// This enum describes the different operational phases for the compiler +/// in order. Anything below `NoOptimization` is considered only for debugging +enum class CompilationPhase { + Parse, + Analysis, + SLIR, + MLIR, // Lowered slir to other dialects + LIR, // Lowered to the llvm ir dialect + IR, // Lowered to the LLVMIR itself + NoOptimization, + O1, + O2, + O3, +}; + /// Options describes the compiler options that can be passed to the /// compiler via command line. Anything that user should be able to /// tweak about the compiler has to end up here regardless of the @@ -34,6 +49,8 @@ struct Options { bool JITenableGDBNotificationListener = true; bool JITenablePerfNotificationListener = true; bool JITLazy = false; + + CompilationPhase compilationPhase = CompilationPhase::NoOptimization; }; } // namespace serene