serene/libserene/lib/jit/halley.cpp

688 lines
23 KiB
C++

/* -*- C++ -*-
* Serene Programming Language
*
* Copyright (c) 2019-2022 Sameer Rahmani <lxsameer@gnu.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "serene/jit/halley.h"
#include "serene/context.h" // for Seren...
#include "serene/fs.h"
#include "serene/options.h" // for Options
#include "serene/types/types.h" // for Names...
#include <system_error> // for error...
#include <llvm/ADT/StringMapEntry.h> // for Strin...
#include <llvm/ADT/StringRef.h>
#include <llvm/ADT/Triple.h> // for Triple
#include <llvm/ADT/iterator.h> // for itera...
#include <llvm/BinaryFormat/Magic.h>
#include <llvm/ExecutionEngine/JITEventListener.h> // for JITEv...
#include <llvm/ExecutionEngine/Orc/CompileUtils.h> // for TMOwn...
#include <llvm/ExecutionEngine/Orc/Core.h> // for Execu...
#include <llvm/ExecutionEngine/Orc/DebugUtils.h> // for opera...
#include <llvm/ExecutionEngine/Orc/ExecutionUtils.h> // for Dynam...
#include <llvm/ExecutionEngine/Orc/IRCompileLayer.h> // for IRCom...
#include <llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h> // for JITTa...
#include <llvm/ExecutionEngine/Orc/LLJIT.h> // for LLJIT...
#include <llvm/ExecutionEngine/Orc/ObjectFileInterface.h>
#include <llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h> // for RTDyl...
#include <llvm/ExecutionEngine/Orc/ThreadSafeModule.h> // for Threa...
#include <llvm/ExecutionEngine/SectionMemoryManager.h> // for Secti...
#include <llvm/IR/DataLayout.h> // for DataL...
#include <llvm/IR/LLVMContext.h> // for LLVMC...
#include <llvm/IR/Module.h> // for Module
#include <llvm/IRReader/IRReader.h>
#include <llvm/Support/CodeGen.h> // for Level
#include <llvm/Support/Error.h>
#include <llvm/Support/FileSystem.h> // for OF_None
#include <llvm/Support/FormatVariadic.h> // for formatv
#include <llvm/Support/MemoryBuffer.h>
#include <llvm/Support/SourceMgr.h>
#include <llvm/Support/ToolOutputFile.h> // for ToolO...
#include <llvm/Support/raw_ostream.h> // for raw_o...
#include <algorithm> // for max
#include <assert.h> // for assert
#include <cerrno>
#include <cstring>
#include <gc.h>
#include <memory> // for uniqu...
#include <string> // for opera...
#include <utility> // for move
#define COMMON_ARGS_COUNT 8
namespace serene {
namespace jit {
// TODO: [error] Replace this function when we implemented
// the error subsystem with the official implementation
llvm::Error tempError(SereneContext &ctx, llvm::Twine s) {
(void)ctx;
return llvm::make_error<llvm::StringError>(
std::make_error_code(std::errc::executable_format_error),
"[Error]: " + s);
};
// /TODO
void ObjectCache::notifyObjectCompiled(const llvm::Module *m,
llvm::MemoryBufferRef objBuffer) {
cachedObjects[m->getModuleIdentifier()] =
llvm::MemoryBuffer::getMemBufferCopy(objBuffer.getBuffer(),
objBuffer.getBufferIdentifier());
}
std::unique_ptr<llvm::MemoryBuffer>
ObjectCache::getObject(const llvm::Module *m) {
auto i = cachedObjects.find(m->getModuleIdentifier());
if (i == cachedObjects.end()) {
HALLEY_LOG("No object for " + m->getModuleIdentifier() +
" in cache. Compiling.");
return nullptr;
}
HALLEY_LOG("Object for " + m->getModuleIdentifier() + " loaded from cache.");
return llvm::MemoryBuffer::getMemBuffer(i->second->getMemBufferRef());
}
void ObjectCache::dumpToObjectFile(llvm::StringRef outputFilename) {
// Set up the output file.
std::error_code error;
auto file = std::make_unique<llvm::ToolOutputFile>(outputFilename, error,
llvm::sys::fs::OF_None);
if (error) {
llvm::errs() << "cannot open output file '" + outputFilename.str() +
"': " + error.message()
<< "\n";
return;
}
// Dump the object generated for a single module to the output file.
// TODO: Replace this with a runtime check
assert(cachedObjects.size() == 1 && "Expected only one object entry.");
auto &cachedObject = cachedObjects.begin()->second;
file->os() << cachedObject->getBuffer();
file->keep();
}
llvm::orc::JITDylib *Halley::getLatestJITDylib(const types::Namespace &ns) {
return getLatestJITDylib(ns.name->data);
};
llvm::orc::JITDylib *Halley::getLatestJITDylib(const char *nsName) {
if (jitDylibs.count(nsName) == 0) {
return nullptr;
}
auto vec = jitDylibs[nsName];
// TODO: Make sure that the returning Dylib still exists in the JIT
// by calling jit->engine->getJITDylibByName(dylib_name);
return vec.empty() ? nullptr : vec.back();
};
void Halley::pushJITDylib(types::Namespace &ns, llvm::orc::JITDylib *l) {
if (jitDylibs.count(ns.name->data) == 0) {
llvm::SmallVector<llvm::orc::JITDylib *, 1> vec{l};
jitDylibs[ns.name->data] = vec;
return;
}
auto vec = jitDylibs[ns.name->data];
vec.push_back(l);
jitDylibs[ns.name->data] = vec;
}
size_t Halley::getNumberOfJITDylibs(types::Namespace &ns) {
if (jitDylibs.count(ns.name->data) == 0) {
return 0;
}
return jitDylibs[ns.name->data].size();
};
Halley::Halley(std::unique_ptr<SereneContext> ctx,
llvm::orc::JITTargetMachineBuilder &&jtmb, llvm::DataLayout &&dl)
: cache(ctx->opts.JITenableObjectCache ? new ObjectCache() : nullptr),
gdbListener(ctx->opts.JITenableGDBNotificationListener
? llvm::JITEventListener::createGDBRegistrationListener()
: nullptr),
perfListener(ctx->opts.JITenablePerfNotificationListener
? llvm::JITEventListener::createPerfJITEventListener()
: nullptr),
jtmb(jtmb), dl(dl), ctx(std::move(ctx)){};
// MaybeJITPtr Halley::lookup(exprs::Symbol &sym) const {
// HALLEY_LOG("Looking up: " << sym.toString());
// auto *ns = ctx.getNS(sym.nsName);
// if (ns == nullptr) {
// return errors::makeError(ctx, errors::CantResolveSymbol, sym.location,
// "Can't find the namespace in the context: " +
// sym.nsName);
// }
// auto *dylib = ctx.getLatestJITDylib(*ns);
// //
// if (dylib == nullptr) {
// return errors::makeError(ctx, errors::CantResolveSymbol, sym.location,
// "Don't know about namespace: " + sym.nsName);
// }
// auto expectedSymbol =
// engine->lookup(*dylib, makePackedFunctionName(sym.name));
// // JIT lookup may return an Error referring to strings stored internally by
// // the JIT. If the Error outlives the ExecutionEngine, it would want have a
// // dangling reference, which is currently caught by an assertion inside JIT
// // thanks to hand-rolled reference counting. Rewrap the error message into
// a
// // string before returning. Alternatively, ORC JIT should consider copying
// // the string into the error message.
// if (!expectedSymbol) {
// std::string errorMessage;
// llvm::raw_string_ostream os(errorMessage);
// llvm::handleAllErrors(expectedSymbol.takeError(),
// [&os](llvm::ErrorInfoBase &ei) { ei.log(os); });
// return errors::makeError(ctx, errors::CantResolveSymbol, sym.location,
// os.str());
// }
// auto rawFPtr = expectedSymbol->getValue();
// // NOLINTNEXTLINE(performance-no-int-to-ptr)
// auto fptr = reinterpret_cast<void (*)(void **)>(rawFPtr);
// if (fptr == nullptr) {
// return errors::makeError(ctx, errors::CantResolveSymbol, sym.location,
// "Lookup function is null!");
// }
// return fptr;
// };
void Halley::setEngine(std::unique_ptr<llvm::orc::LLJIT> e, bool isLazy) {
// Later on we might use different classes of JIT which might need some
// work for lazyness
(void)ctx;
engine = std::move(e);
this->isLazy = isLazy;
};
void Halley::dumpToObjectFile(llvm::StringRef filename) {
cache->dumpToObjectFile(filename);
};
MaybeEngine Halley::make(std::unique_ptr<SereneContext> sereneCtxPtr,
llvm::orc::JITTargetMachineBuilder &&jtmb) {
auto dl = jtmb.getDefaultDataLayoutForTarget();
if (!dl) {
return dl.takeError();
}
auto jitEngine = std::make_unique<Halley>(std::move(sereneCtxPtr),
std::move(jtmb), std::move(*dl));
// Why not the llvmcontext from the SereneContext??
// Sice we're going to pass the ownership of this context to a thread
// safe module later on and we will only create the jit function wrappers
// with it, then it is fine to use a new context.
//
// 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<llvm::LLVMContext> ctx(new llvm::LLVMContext);
// Since we moved the original sereneCtxPtr into the engine.
auto &sereneCtx = jitEngine->getContext();
// 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<llvm::orc::RTDyldObjectLinkingLayer>(session, []() {
return std::make_unique<llvm::SectionMemoryManager>();
});
// 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 (sereneCtx.triple.isOSBinFormatCOFF()) {
objectLayer->setOverrideObjectFlagsWithResponsibilityFlags(true);
objectLayer->setAutoClaimResponsibilityForObjectSymbols(true);
}
// Resolve symbols from shared libraries.
// for (auto libPath : sharedLibPaths) {
// auto mb = llvm::MemoryBuffer::getFile(libPath);
// if (!mb) {
// llvm::errs() << "Failed to create MemoryBuffer for: " << libPath
// << "\nError: " << mb.getError().message() << "\n";
// continue;
// }
// auto &JD = session.createBareJITDylib(std::string(libPath));
// auto loaded = llvm::orc::DynamicLibrarySearchGenerator::Load(
// libPath.data(), dataLayout.getGlobalPrefix());
// if (!loaded) {
// llvm::errs() << "Could not load " << libPath << ":\n "
// << loaded.takeError() << "\n";
// continue;
// }
// JD.addGenerator(std::move(*loaded));
// cantFail(objectLayer->add(JD, std::move(mb.get())));
// }
return objectLayer;
};
// Callback to inspect the cache and recompile on demand. This follows
// Lang's LLJITWithObjectCache example.
auto compileFunctionCreator = [&](llvm::orc::JITTargetMachineBuilder JTMB)
-> llvm::Expected<
std::unique_ptr<llvm::orc::IRCompileLayer::IRCompiler>> {
llvm::CodeGenOpt::Level jitCodeGenOptLevel =
static_cast<llvm::CodeGenOpt::Level>(sereneCtx.getOptimizatioLevel());
JTMB.setCodeGenOptLevel(jitCodeGenOptLevel);
auto targetMachine = JTMB.createTargetMachine();
if (!targetMachine) {
return targetMachine.takeError();
}
return std::make_unique<llvm::orc::TMOwningSimpleCompiler>(
std::move(*targetMachine), jitEngine->cache.get());
};
if (sereneCtx.opts.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());
jitEngine->setEngine(std::move(jit), true);
} 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());
jitEngine->setEngine(std::move(jit), false);
}
jitEngine->engine->getIRCompileLayer().setNotifyCompiled(
[&](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());
});
});
// Resolve symbols that are statically linked in the current process.
llvm::orc::JITDylib &mainJD = jitEngine->engine->getMainJITDylib();
mainJD.addGenerator(
cantFail(llvm::orc::DynamicLibrarySearchGenerator::GetForCurrentProcess(
jitEngine->dl.getGlobalPrefix())));
return MaybeEngine(std::move(jitEngine));
};
const types::InternalString &Halley::getInternalString(const char *s) {
// TODO: [serene.core] We need to provide some functions on llvm level to
// build instances from these type in a functional way. We need to avoid
// randomly build instances here and there that causes unsafe memory
assert(s && "s is nullptr: getInternalString");
auto len = std::strlen(s);
auto *str =
(types::InternalString *)GC_MALLOC_ATOMIC(sizeof(types::InternalString));
str->data = (char *)GC_MALLOC_ATOMIC(len);
memcpy((void *)str->data, (void *)s, len);
str->len = len;
stringStorage.push_back(str);
return *str;
// /TODO
};
types::Namespace &Halley::makeNamespace(const char *name) {
// TODO: [serene.core] We need to provide some functions on llvm level to
// build instances from these type in a functional way. We need to avoid
// randomly build instances here and there that causes unsafe memory
assert(name && "name is nullptr: createNamespace");
const auto &nsName = getInternalString(name);
auto *ns = (types::Namespace *)GC_MALLOC(sizeof(types::Namespace));
ns->name = &nsName;
nsStorage.push_back(ns);
return *ns;
// /TODO
};
llvm::Error Halley::createEmptyNS(const char *name) {
assert(name && "name is nullptr: createEmptyNS");
// TODO: Look up the Namespace first.
auto &ns = makeNamespace(name);
auto numOfDylibs = getNumberOfJITDylibs(ns) + 1;
HALLEY_LOG(
llvm::formatv("Creating Dylib {0}#{1}", ns.name->data, numOfDylibs));
auto newDylib = engine->createJITDylib(
llvm::formatv("{0}#{1}", ns.name->data, numOfDylibs));
if (!newDylib) {
llvm::errs() << "Couldn't create the jitDylib\n";
serene::terminate(*ctx, 1);
}
pushJITDylib(ns, &(*newDylib));
return llvm::Error::success();
};
MaybeEnginePtr Halley::lookup(const char *nsName, const char *sym) {
assert(sym != nullptr && "'sym' is null: lookup");
assert(nsName != nullptr && "'nsName' is null: lookup");
llvm::StringRef s{sym};
llvm::StringRef ns{nsName};
std::string fqsym = (ns + "/" + s).str();
auto *dylib = jitDylibs[nsName].back();
if (dylib == nullptr) {
return tempError(*ctx, "No dylib " + s);
}
auto expectedSymbol = engine->lookup(*dylib, fqsym);
// JIT lookup may return an Error referring to strings stored internally by
// the JIT. If the Error outlives the ExecutionEngine, it would want have a
// dangling reference, which is currently caught by an assertion inside JIT
// thanks to hand-rolled reference counting. Rewrap the error message into a
// string before returning. Alternatively, ORC JIT should consider copying
// the string into the error message.
if (!expectedSymbol) {
return expectedSymbol.takeError();
}
auto rawFPtr = *expectedSymbol;
// NOLINTNEXTLINE(performance-no-int-to-ptr)
auto fptr = reinterpret_cast<void *(*)()>(&rawFPtr);
if (fptr == nullptr) {
return tempError(*ctx, "Lookup function is null!");
}
return fptr;
};
// TODO: Remove this function before prod release
llvm::Error Halley::loadModule(const char *nsName, const char *file) {
assert(file && "'file' is nullptr: loadModule");
assert(nsName && "'nsName' is nullptr: loadModule");
auto llvmContext = ctx->genLLVMContext();
llvm::SMDiagnostic error;
auto *dylib = jitDylibs[nsName].back();
auto module = llvm::parseIRFile(file, error, *llvmContext);
if (module == nullptr) {
return llvm::make_error<llvm::StringError>(
std::make_error_code(std::errc::executable_format_error),
error.getMessage().str() + " File: " + file);
}
auto tsm =
llvm::orc::ThreadSafeModule(std::move(module), std::move(llvmContext));
return engine->addIRModule(*dylib, std::move(tsm));
};
// /TODO
// TODO: [error] Remove this function when we implemented
// the error subsystem
llvm::Error NotImplemented(llvm::StringRef s) {
return llvm::make_error<llvm::StringError>(
std::make_error_code(std::errc::executable_format_error),
"Not Implemented: " + s);
};
template <>
MaybeDylibPtr
Halley::loadNamespaceFrom<fs::NSFileType::Source>(NSLoadRequest &req) {
(void)req;
return nullptr;
};
template <>
MaybeDylibPtr
Halley::loadNamespaceFrom<fs::NSFileType::TextIR>(NSLoadRequest &req) {
(void)req;
return nullptr;
};
template <>
MaybeDylibPtr
Halley::loadNamespaceFrom<fs::NSFileType::BinaryIR>(NSLoadRequest &req) {
(void)req;
return nullptr;
};
template <>
MaybeDylibPtr
Halley::loadNamespaceFrom<fs::NSFileType::ObjectFile>(NSLoadRequest &req) {
auto file = fs::join(req.path, req.nsToFileName + ".o");
if (!fs::exists(file)) {
// Can't locate any object file, skit to the next loader
llvm::outs() << "file: " << file << "\n";
return nullptr;
}
auto err = createEmptyNS(req.nsName.str().c_str());
if (err) {
return err;
}
auto *jd = getLatestJITDylib(req.nsName.str().c_str());
assert(jd != nullptr && "'jd' must not be null since we just created it.");
auto buf = llvm::errorOrToExpected(llvm::MemoryBuffer::getFile(file));
if (!buf) {
return buf.takeError();
}
err = engine->getObjLinkingLayer().add(*jd, std::move(*buf));
if (err) {
return err;
}
llvm::outs() << "ok\n";
return jd;
};
template <>
MaybeDylibPtr
Halley::loadNamespaceFrom<fs::NSFileType::StaticLib>(NSLoadRequest &req) {
// Skip missing or non-regular paths.
if (llvm::sys::fs::get_file_type(req.path) !=
llvm::sys::fs::file_type::regular_file) {
return tempError(*ctx, "Not a regular file: " + req.path);
}
llvm::file_magic magic;
if (auto ec = llvm::identify_magic(req.path, magic)) {
// If there was an error loading the file then skip it.
return tempError(*ctx,
ec.message() + "\nFile Identification Erorr: " + req.path);
}
if (magic != llvm::file_magic::archive ||
magic != llvm::file_magic::macho_universal_binary) {
return tempError(*ctx, "Not a static lib: " + req.path);
}
auto err = createEmptyNS(req.nsName.str().c_str());
if (err) {
return err;
}
auto &session = engine->getExecutionSession();
auto *jd = getLatestJITDylib(req.nsName.str().c_str());
assert(jd == nullptr && "'jd' must not be null since we just created it.");
// TODO: Handle hidden static libs as well look at the addLibrary/AddArchive
// in llvm-jitlink
auto generator = llvm::orc::StaticLibraryDefinitionGenerator::Load(
engine->getObjLinkingLayer(), req.path.str().c_str(),
session.getExecutorProcessControl().getTargetTriple(),
std::move(llvm::orc::getObjectFileInterface));
if (!generator) {
return generator.takeError();
}
jd->addGenerator(std::move(*generator));
return jd;
};
template <>
MaybeDylibPtr
Halley::loadNamespaceFrom<fs::NSFileType::SharedLib>(NSLoadRequest &req) {
(void)req;
// switch (magic) {
// case llvm::file_magic::elf_shared_object:
// case llvm::file_magic::macho_dynamically_linked_shared_lib: {
// // TODO: On first reference to LibPath this should create a JITDylib
// // with a generator and add it to JD's links-against list. Subsquent
// // references should use the JITDylib created on the first
// // reference.
// auto g = llvm::EPCDynamicLibrarySearchGenerator::Load(session, path);
// if (!g)
// return g.takeError();
// jd.addGenerator(std::move(*g));
// break;
// }
// case llvm::file_magic::archive:
// case llvm::file_magic::macho_universal_binary: {
// }
// default:
// // This file isn't a recognized library kind.
// LibFound = false;
// break;
// }
return NotImplemented("loadNamespaceFrom<shared>");
};
MaybeDylibPtr Halley::loadNamespaceFrom(fs::NSFileType type_,
NSLoadRequest &req) {
switch (type_) {
case fs::NSFileType::Source:
return loadNamespaceFrom<fs::NSFileType::Source>(req);
case fs::NSFileType::TextIR:
return loadNamespaceFrom<fs::NSFileType::TextIR>(req);
case fs::NSFileType::BinaryIR:
return loadNamespaceFrom<fs::NSFileType::BinaryIR>(req);
case fs::NSFileType::ObjectFile:
return loadNamespaceFrom<fs::NSFileType::ObjectFile>(req);
case fs::NSFileType::StaticLib:
case fs::NSFileType::SharedLib:
return loadNamespaceFrom<fs::NSFileType::StaticLib>(req);
};
};
MaybeDylibPtr Halley::loadNamespace(std::string &nsName) {
if (ctx->getLoadPaths().empty()) {
return tempError(*ctx, "Load paths should not be empty");
}
for (auto &path : ctx->getLoadPaths()) {
auto nsFileName = fs::namespaceToPath(nsName);
NSLoadRequest req{nsName, path, nsFileName};
for (auto type_ : {fs::NSFileType::Source, fs::NSFileType::ObjectFile}) {
auto maybeJDptr = loadNamespaceFrom(type_, req);
if (!maybeJDptr) {
return maybeJDptr.takeError();
}
if (*maybeJDptr != nullptr) {
return *maybeJDptr;
}
}
}
return tempError(*ctx, "Can't find namespace: " + nsName);
};
llvm::Error Halley::initialize() {
(void)ctx;
return llvm::Error::success();
};
MaybeEngine makeHalleyJIT(std::unique_ptr<SereneContext> ctx) {
llvm::orc::JITTargetMachineBuilder jtmb(ctx->triple);
auto maybeJIT = Halley::make(std::move(ctx), std::move(jtmb));
if (!maybeJIT) {
return maybeJIT.takeError();
}
return maybeJIT;
};
} // namespace jit
} // namespace serene