diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24616df..fdae71f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: cxx: "cl", boost_toolset: msvc, cmake_generator: "Visual Studio 17 2022", - llvm_tag: "llvmorg-18.1.8", + llvm_tag: "llvmorg-19.1.4", python_version: "3.10", os_version: "2022", artifact_id: "RG3_Windows" @@ -35,7 +35,7 @@ jobs: cxx: "g++-13", boost_toolset: gcc, cmake_generator: "Ninja", - llvm_tag: "llvmorg-18.1.8", + llvm_tag: "llvmorg-19.1.4", python_version: "3.10", os_version: "24.04", artifact_id: "RG3_Linux" @@ -48,7 +48,7 @@ jobs: cxx: "clang++", boost_toolset: gcc, cmake_generator: "Ninja", - llvm_tag: "llvmorg-18.1.8", + llvm_tag: "llvmorg-19.1.4", python_version: "3.10", os_version: "13", artifact_id: "RG3_macOS" diff --git a/Extension/README.md b/Extension/README.md index fe99db4..bc72a1f 100644 --- a/Extension/README.md +++ b/Extension/README.md @@ -12,7 +12,7 @@ Install Make sure that your system has clang (any version): * **macOS**: you need to install XCode (tested on 15.x but should work everywhere) * **Window**: you need to install clang 17.x or later and add it into PATH - * **Linux**: gcc & g++ at least 13 version (temporary limitation, planned to fix at 0.0.4) + * **Linux**: gcc & g++ at least 13 version * **Other platforms & archs**: Contact us in [our GitHub](https://github.com/DronCode/RG3). It's a better way to use RG3 inside virtualenv: @@ -99,16 +99,53 @@ We have a type my::cool::name_space::SomeThirdPartyStruct (TK_STRUCT_OR_CLASS) Function static bool IsGeniusDesc(bool bCanReplace) ``` +Another good use case is `constexpr` evaluation feature: +```python +import rg3py +from typing import Union, List, Dict + +evaluator: rg3py.CodeEvaluator = rg3py.CodeEvaluator.make_from_system_env() + +evaluator.set_compiler_config({ + "cpp_standard" : rg3py.CppStandard.CXX_20 +}) + +result: Union[Dict[str, any], List[rg3py.CppCompilerIssue]] = evaluator.eval(""" + #include + + class Simple {}; + class Base {}; + class Inherited : public Base {}; + + constexpr bool r0 = std::is_base_of_v; + constexpr bool r1 = std::is_base_of_v; + """, ["r0", "r1"]) + +print(result) +``` + +output will be + +```text +{'r1': False, 'r0': True} +``` + +and that's great feature to make some checks like `type should be inherited from A, have some methods and etc...` + +And this is independent of your current environment, only C++ STL library should be found! + + Features --------- * Supported Windows (x86_64), Linux (x86_64) and macOS (x86_64 and ARM64) - * Supported C++03, 11, 14, 17, 20, 23 (26 in theory, need to migrate to next LLVM) + * Supported C++03, 11, 14, 17, 20, 23, 26 * Supported threads in analysis on native side (see Tests/PyIntegration/test.py test: **test_analyzer_context_sample** for example) * Statically linked, no external dependencies (except Clang instance on machine) * Special macro definitions to hide unnecessary code * Template specializations reporting * Anonymous registration without changes in third party code + * Runtime constexpr C++ code evaluation with results extraction Current limitations ------------------- @@ -118,8 +155,8 @@ Project focused on work around C/C++ headers (C++ especially). Feel free to fork Third Party libraries ---------------------- - * LLVM 16.0.4 - our main backend of C++ analysis - * Boost 1.81.0 - python support & process launcher - * FMT - string formatter + * LLVM - our main backend of C++ analysis + * Boost.Python - python support & process launcher + * fmt - string formatter * googletest - for internal unit testing * pytest - for python side unit testing \ No newline at end of file diff --git a/LLVM/include/RG3/LLVM/Actions/CollectConstexprVariableEvalResultAction.h b/LLVM/include/RG3/LLVM/Actions/CollectConstexprVariableEvalResultAction.h new file mode 100644 index 0000000..53a3091 --- /dev/null +++ b/LLVM/include/RG3/LLVM/Actions/CollectConstexprVariableEvalResultAction.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include +#include + + + +namespace rg3::llvm::actions +{ + struct CollectConstexprVariableEvalResultAction : public clang::ASTFrontendAction + { + std::unordered_set aExpectedVariables {}; + std::unordered_map* pEvaluatedVariables { nullptr }; + + std::unique_ptr CreateASTConsumer(clang::CompilerInstance& /*compilerInstance*/, clang::StringRef /*file*/) override; + }; +} \ No newline at end of file diff --git a/LLVM/include/RG3/LLVM/CodeEvaluator.h b/LLVM/include/RG3/LLVM/CodeEvaluator.h new file mode 100644 index 0000000..bc84426 --- /dev/null +++ b/LLVM/include/RG3/LLVM/CodeEvaluator.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + + +namespace rg3::llvm +{ + using VariableValue = std::variant; + + struct CodeEvaluateResult + { + AnalyzerResult::CompilerIssuesVector vIssues; + std::unordered_map mOutputs; + + explicit operator bool() const noexcept; + }; + + class CodeEvaluator : public boost::noncopyable + { + public: + CodeEvaluator(); + CodeEvaluator(CompilerConfig compilerConfig); + + void setCompilerEnvironment(const CompilerEnvironment& env); + CompilerConfig& getCompilerConfig(); + const CompilerConfig& getCompilerConfig() const; + + CodeEvaluateResult evaluateCode(const std::string& sCode, const std::vector& aCaptureOutputVariables); + + private: + std::optional m_env; + CompilerConfig m_compilerConfig; + std::string m_sSourceCode {}; + }; +} \ No newline at end of file diff --git a/LLVM/include/RG3/LLVM/CompilerInstanceFactory.h b/LLVM/include/RG3/LLVM/CompilerInstanceFactory.h new file mode 100644 index 0000000..8171498 --- /dev/null +++ b/LLVM/include/RG3/LLVM/CompilerInstanceFactory.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include +#include + +#include +#include +#include + + +namespace rg3::llvm +{ + struct CompilerInstanceFactory + { + static void makeInstance( + clang::CompilerInstance* pOutInstance, + const std::variant& sInput, + const CompilerConfig& sCompilerConfig, + const CompilerEnvironment* pCompilerEnv = nullptr); + }; +} \ No newline at end of file diff --git a/LLVM/include/RG3/LLVM/Consumers/CollectConstexprVariableEvalResult.h b/LLVM/include/RG3/LLVM/Consumers/CollectConstexprVariableEvalResult.h new file mode 100644 index 0000000..52c183f --- /dev/null +++ b/LLVM/include/RG3/LLVM/Consumers/CollectConstexprVariableEvalResult.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include +#include + + +namespace rg3::llvm::consumers +{ + struct CollectConstexprVariableEvalResult : public clang::ASTConsumer + { + std::unordered_set aExpectedVariables {}; + std::unordered_map* pEvaluatedVariables { nullptr }; + + public: + CollectConstexprVariableEvalResult(); + + void HandleTranslationUnit(clang::ASTContext& ctx) override; + }; +} \ No newline at end of file diff --git a/LLVM/source/Actions/CollectConstexprVariableEvalResultAction.cpp b/LLVM/source/Actions/CollectConstexprVariableEvalResultAction.cpp new file mode 100644 index 0000000..0face2f --- /dev/null +++ b/LLVM/source/Actions/CollectConstexprVariableEvalResultAction.cpp @@ -0,0 +1,15 @@ +#include +#include + + +namespace rg3::llvm::actions +{ + std::unique_ptr CollectConstexprVariableEvalResultAction::CreateASTConsumer(clang::CompilerInstance&, clang::StringRef) + { + auto pConsumer = std::make_unique(); + pConsumer->aExpectedVariables = aExpectedVariables; + pConsumer->pEvaluatedVariables = pEvaluatedVariables; + + return std::move(pConsumer); + } +} \ No newline at end of file diff --git a/LLVM/source/CodeAnalyzer.cpp b/LLVM/source/CodeAnalyzer.cpp index 403962b..1d2daa6 100644 --- a/LLVM/source/CodeAnalyzer.cpp +++ b/LLVM/source/CodeAnalyzer.cpp @@ -1,81 +1,17 @@ #include #include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include #include -#include /// Auto-generated by CMake - #include #include namespace rg3::llvm { - struct Visitor - { - clang::FrontendOptions& compilerOptions; - - void operator()(const std::filesystem::path& path) - { - std::string absolutePath = std::filesystem::absolute(path).string(); - - compilerOptions.Inputs.push_back( - clang::FrontendInputFile( - absolutePath, - clang::InputKind( - clang::Language::CXX, - clang::InputKind::Format::Source, - false, // NOT preprocessed - clang::InputKind::HeaderUnitKind::HeaderUnit_User, - true // is Header = true - ), - false // IsSystem = false - ) - ); - } - - void operator()(const std::string& buffer) - { - compilerOptions.Inputs.push_back( - clang::FrontendInputFile( - ::llvm::MemoryBufferRef( - ::llvm::StringRef(buffer.data()), - ::llvm::StringRef("id0.hpp") - ), - clang::InputKind( - clang::Language::CXX, - clang::InputKind::Format::Source, - false, // NOT preprocessed - clang::InputKind::HeaderUnitKind::HeaderUnit_User, - true // is Header = true - ), - false // IsSystem = false - ) - ); - } - }; - AnalyzerResult::operator bool() const { return std::count_if( @@ -145,8 +81,9 @@ namespace rg3::llvm AnalyzerResult CodeAnalyzer::analyze() { AnalyzerResult result; - const CompilerEnvironment* pCompilerEnv = nullptr; - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CompilerEnvironment* pCompilerEnv = nullptr; + // Run platform env detector if (!m_env.has_value()) { @@ -163,228 +100,15 @@ namespace rg3::llvm } pCompilerEnv = &m_env.value(); + clang::CompilerInstance compilerInstance {}; + CompilerInstanceFactory::makeInstance(&compilerInstance, m_source, m_compilerConfig, pCompilerEnv); - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - clang::CompilerInstance compilerInstance; - compilerInstance.createDiagnostics(); - - // Create custom diagnostics consumer and pass it into CompilerInstance + // Add diagnostics consumer { auto errorCollector = std::make_unique(result); compilerInstance.getDiagnostics().setClient(errorCollector.release(), false); } - // Set up FileManager and SourceManager - compilerInstance.createFileManager(); - compilerInstance.createSourceManager(compilerInstance.getFileManager()); - - std::vector vProxyArgs = m_compilerConfig.vCompilerArgs; -#ifdef _WIN32 - vProxyArgs.emplace_back("-fms-extensions"); - vProxyArgs.emplace_back("-fdelayed-template-parsing"); - vProxyArgs.emplace_back("-fms-compatibility-version=19"); -#endif - - if (auto it = std::find(vProxyArgs.begin(), vProxyArgs.end(), "-x"); it != vProxyArgs.end()) - { - // need to remove this iter and next - auto next = std::next(it); - - if (next != std::end(vProxyArgs)) - { - // remove next - vProxyArgs.erase(next); - } - - // and remove this - vProxyArgs.erase(it); - } - - // We will append "-x c++-header" option always - vProxyArgs.emplace_back("-x"); - vProxyArgs.emplace_back("c++-header"); - - std::vector vCompilerArgs; - vCompilerArgs.resize(vProxyArgs.size()); - - for (size_t i = 0; i < vProxyArgs.size(); i++) - { - vCompilerArgs[i] = vProxyArgs[i].c_str(); - } - - // Set up CompilerInvocation - auto invocation = std::make_shared(); - clang::CompilerInvocation::CreateFromArgs( - *invocation, - ::llvm::ArrayRef(vCompilerArgs.data(), vCompilerArgs.size()), - compilerInstance.getDiagnostics() - ); - - // Use C++20 - clang::LangStandard::Kind langKind; - auto& langOptions = invocation->getLangOpts(); - - langOptions.CPlusPlus = 1; - - switch (m_compilerConfig.cppStandard) - { - case CxxStandard::CC_11: - langOptions.CPlusPlus11 = 1; - langKind = clang::LangStandard::Kind::lang_cxx11; - break; - case CxxStandard::CC_14: - langOptions.CPlusPlus14 = 1; - langKind = clang::LangStandard::Kind::lang_cxx14; - break; - case CxxStandard::CC_17: - langOptions.CPlusPlus17 = 1; - langKind = clang::LangStandard::Kind::lang_cxx17; - break; - case CxxStandard::CC_20: - langOptions.CPlusPlus20 = 1; - langKind = clang::LangStandard::Kind::lang_cxx20; - break; - case CxxStandard::CC_23: - langOptions.CPlusPlus23 = 1; - langKind = clang::LangStandard::Kind::lang_cxx23; - break; - case CxxStandard::CC_26: - langOptions.CPlusPlus26 = 1; - langKind = clang::LangStandard::Kind::lang_cxx26; - break; - default: - langOptions.CPlusPlus11 = 1; - langKind = clang::LangStandard::Kind::lang_cxx11; - break; - } - - langOptions.LangStd = langKind; - langOptions.IsHeaderFile = true; // NOTE: Maybe we should use flag here? - -#ifdef _WIN32 - compilerInstance.getPreprocessorOpts().addMacroDef("_MSC_VER=1932"); - compilerInstance.getPreprocessorOpts().addMacroDef("_MSC_FULL_VER=193231329"); - compilerInstance.getPreprocessorOpts().addMacroDef("_MSC_EXTENSIONS"); - - /** - * Workaround: it's workaround for MSVC 2022 with yvals_core.h which send static assert when Clang version less than 17.x.x - * Example : static assertion failed: error STL1000: Unexpected compiler version, expected Clang 17.0.0 or newer. at C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.40.33807\include\yvals_core.h:898 - * - * This macro block static assertion and should help us, but this part of code MUST be removed after RG3 migrates to latest LLVM & Clang - */ - compilerInstance.getPreprocessorOpts().addMacroDef("_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH=1"); -#endif - -#ifdef __APPLE__ - // This should be enough? - compilerInstance.getPreprocessorOpts().addMacroDef("__GCC_HAVE_DWARF2_CFI_ASM=1"); -#endif - - std::shared_ptr targetOpts = nullptr; - - // Setup triple -#if !defined(__APPLE__) - if (pCompilerEnv->triple.empty()) - { - // Use default triple - targetOpts = std::make_shared(); - targetOpts->Triple = ::llvm::sys::getDefaultTargetTriple(); - clang::TargetInfo* targetInfo = clang::TargetInfo::CreateTargetInfo(compilerInstance.getDiagnostics(), targetOpts); - compilerInstance.setTarget(targetInfo); - - auto triple = targetInfo->getTriple(); - - std::vector vIncs; - clang::LangOptions::setLangDefaults(langOptions, clang::Language::CXX, triple, vIncs, langKind); - } - else - { - targetOpts = std::make_shared(); - targetOpts->Triple = ::llvm::Triple::normalize(pCompilerEnv->triple); - clang::TargetInfo* targetInfo = clang::TargetInfo::CreateTargetInfo(compilerInstance.getDiagnostics(), targetOpts); - compilerInstance.setTarget(targetInfo); - - auto triple = targetInfo->getTriple(); - - std::vector vIncs; - clang::LangOptions::setLangDefaults(langOptions, clang::Language::CXX, triple, vIncs, langKind); - } -#else - // On Apple we should use default triple instead of detect it at runtime - ::llvm::Triple triple(::llvm::sys::getDefaultTargetTriple()); - compilerInstance.getTargetOpts().Triple = triple.str(); - compilerInstance.setTarget(clang::TargetInfo::CreateTargetInfo(compilerInstance.getDiagnostics(), std::make_shared(compilerInstance.getTargetOpts()))); -#endif - - compilerInstance.setInvocation(invocation); - - // Set up FrontendOptions - clang::FrontendOptions &opts = compilerInstance.getFrontendOpts(); - opts.ProgramAction = clang::frontend::ParseSyntaxOnly; - opts.SkipFunctionBodies = static_cast(m_compilerConfig.bSkipFunctionBodies); - - opts.Inputs.clear(); - - // Prepare compiler instance - { - Visitor v { opts }; - std::visit(v, m_source); - } - - // Set macros - clang::PreprocessorOptions& preprocessorOptions = compilerInstance.getPreprocessorOpts(); - for (const auto& compilerDef : m_compilerConfig.vCompilerDefs) - { - preprocessorOptions.addMacroDef(compilerDef); - } - - // Add builtins - preprocessorOptions.addMacroDef("__RG3__=1"); - preprocessorOptions.addMacroDef("__RG3_COMMIT__=\"" RG3_BUILD_HASH "\""); - preprocessorOptions.addMacroDef("__RG3_BUILD_DATE__=\"" __DATE__ "\""); - -#ifdef __APPLE__ - // For apple only. They cares about GNUC? Idk & I don't care - preprocessorOptions.addMacroDef("__GNUC__=4"); -#endif - - // Setup header dirs source - clang::HeaderSearchOptions& headerSearchOptions = compilerInstance.getHeaderSearchOpts(); - { - for (const auto& sysInc : pCompilerEnv->config.vSystemIncludes) - { - const auto absolutePath = std::filesystem::absolute(sysInc.sFsLocation); - - clang::frontend::IncludeDirGroup group = clang::frontend::IncludeDirGroup::Angled; - - if (sysInc.eKind == IncludeKind::IK_SYSROOT) - { - // ignore sysroot here - continue; - } - - if (sysInc.eKind == IncludeKind::IK_SYSTEM) - { - group = clang::frontend::IncludeDirGroup::System; - } - - if (sysInc.eKind == IncludeKind::IK_C_SYSTEM) - { - group = clang::frontend::IncludeDirGroup::ExternCSystem; - } - - headerSearchOptions.AddPath(absolutePath.string(), group, false, true); - } - - for (const auto& incInfo : m_compilerConfig.vIncludes) - { - // Convert path to absolute - const auto absolutePath = std::filesystem::absolute(incInfo.sFsLocation); - headerSearchOptions.AddPath(absolutePath.string(), clang::frontend::IncludeDirGroup::Angled, false, true); - } - } - // Run actions { rg3::llvm::actions::ExtractTypesFromTUAction findTypesAction { result.vFoundTypes, m_compilerConfig }; diff --git a/LLVM/source/CodeEvaluator.cpp b/LLVM/source/CodeEvaluator.cpp new file mode 100644 index 0000000..b0f8b5a --- /dev/null +++ b/LLVM/source/CodeEvaluator.cpp @@ -0,0 +1,101 @@ +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include + + +namespace rg3::llvm +{ + CodeEvaluateResult::operator bool() const noexcept + { + return std::count_if( + vIssues.begin(), + vIssues.end(), + [](const AnalyzerResult::CompilerIssue& issue) -> bool { + return issue.kind != AnalyzerResult::CompilerIssue::IssueKind::IK_INFO && + issue.kind != AnalyzerResult::CompilerIssue::IssueKind::IK_NONE; + }) == 0; + } + + CodeEvaluator::CodeEvaluator() = default; + + CodeEvaluator::CodeEvaluator(rg3::llvm::CompilerConfig compilerConfig) + : m_compilerConfig(std::move(compilerConfig)) + { + } + + void CodeEvaluator::setCompilerEnvironment(const rg3::llvm::CompilerEnvironment& env) + { + m_env = env; + } + + CompilerConfig& CodeEvaluator::getCompilerConfig() + { + return m_compilerConfig; + } + + const CompilerConfig& CodeEvaluator::getCompilerConfig() const + { + return m_compilerConfig; + } + + CodeEvaluateResult CodeEvaluator::evaluateCode(const std::string& sCode, const std::vector& aCaptureOutputVariables) + { + CodeEvaluateResult sResult {}; + AnalyzerResult sTempResult {}; + CompilerEnvironment* pCompilerEnv = nullptr; + + m_sSourceCode = sCode; + + // Run platform env detector + if (!m_env.has_value()) + { + const auto compilerEnvironment = CompilerConfigDetector::detectSystemCompilerEnvironment(); + if (auto pEnvFailure = std::get_if(&compilerEnvironment)) + { + // Fatal error + sResult.vIssues.emplace_back(AnalyzerResult::CompilerIssue { AnalyzerResult::CompilerIssue::IssueKind::IK_ERROR, m_sSourceCode, 0, 0, pEnvFailure->message }); + return sResult; + } + + // Override env + m_env = *std::get_if(&compilerEnvironment); + } + + pCompilerEnv = &m_env.value(); + clang::CompilerInstance compilerInstance {}; + CompilerInstanceFactory::makeInstance(&compilerInstance, m_sSourceCode, m_compilerConfig, pCompilerEnv); + + // Add extra definition in our case + compilerInstance.getPreprocessorOpts().addMacroDef("__RG3_CODE_EVAL__=1"); + + // Add diagnostics consumer + { + auto errorCollector = std::make_unique(sTempResult); + compilerInstance.getDiagnostics().setClient(errorCollector.release(), false); + } + + // Run actions + { + rg3::llvm::actions::CollectConstexprVariableEvalResultAction collectConstexprVariableEvalResultAction {}; + collectConstexprVariableEvalResultAction.aExpectedVariables = std::unordered_set { aCaptureOutputVariables.begin(), aCaptureOutputVariables.end() }; + collectConstexprVariableEvalResultAction.pEvaluatedVariables = &sResult.mOutputs; + + compilerInstance.ExecuteAction(collectConstexprVariableEvalResultAction); + } + + // Copy result + std::copy(sTempResult.vIssues.begin(), sTempResult.vIssues.end(), std::back_inserter(sResult.vIssues)); + + return sResult; + } +} \ No newline at end of file diff --git a/LLVM/source/CompilerInstanceFactory.cpp b/LLVM/source/CompilerInstanceFactory.cpp new file mode 100644 index 0000000..e1f0989 --- /dev/null +++ b/LLVM/source/CompilerInstanceFactory.cpp @@ -0,0 +1,289 @@ +#include +#include /// Auto-generated by CMake + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +namespace rg3::llvm +{ + struct Visitor + { + clang::FrontendOptions& compilerOptions; + clang::CompilerInstance* pCompilerInstance { nullptr }; + + void operator()(const std::filesystem::path& path) + { + std::string absolutePath = std::filesystem::absolute(path).string(); + + compilerOptions.Inputs.push_back( + clang::FrontendInputFile( + absolutePath, + clang::InputKind( + clang::Language::CXX, + clang::InputKind::Format::Source, + false, // NOT preprocessed + clang::InputKind::HeaderUnitKind::HeaderUnit_User, + true // is Header = true + ), + false // IsSystem = false + ) + ); + } + + void operator()(const std::string& buffer) + { + std::string sanitizedBuffer; + std::remove_copy_if( + buffer.begin(), buffer.end(), + std::back_inserter(sanitizedBuffer), + [](char c) { return c == '\0'; } + ); + + auto pMemBuffer = ::llvm::MemoryBuffer::getMemBufferCopy(sanitizedBuffer, "id0.hpp"); + pCompilerInstance->getPreprocessorOpts().addRemappedFile("id0.hpp", pMemBuffer.release()); + + compilerOptions.Inputs.push_back(clang::FrontendInputFile("id0.hpp", clang::Language::CXX)); + } + }; + + void CompilerInstanceFactory::makeInstance(clang::CompilerInstance* pOutInstance, + const std::variant& sInput, + const rg3::llvm::CompilerConfig& sCompilerConfig, + const rg3::llvm::CompilerEnvironment* pCompilerEnv) + { + pOutInstance->createDiagnostics(); + + // Set up FileManager and SourceManager + pOutInstance->createFileManager(); + pOutInstance->createSourceManager(pOutInstance->getFileManager()); + + std::vector vProxyArgs = sCompilerConfig.vCompilerArgs; +#ifdef _WIN32 + vProxyArgs.emplace_back("-fms-extensions"); + vProxyArgs.emplace_back("-fdelayed-template-parsing"); + vProxyArgs.emplace_back("-fms-compatibility-version=19"); +#endif + + if (auto it = std::find(vProxyArgs.begin(), vProxyArgs.end(), "-x"); it != vProxyArgs.end()) + { + // need to remove this iter and next + auto next = std::next(it); + + if (next != std::end(vProxyArgs)) + { + // remove next + vProxyArgs.erase(next); + } + + // and remove this + vProxyArgs.erase(it); + } + + // We will append "-x c++-header" option always + vProxyArgs.emplace_back("-x"); + vProxyArgs.emplace_back("c++-header"); + + std::vector vCompilerArgs; + vCompilerArgs.resize(vProxyArgs.size()); + + for (size_t i = 0; i < vProxyArgs.size(); i++) + { + vCompilerArgs[i] = vProxyArgs[i].c_str(); + } + + // Set up CompilerInvocation + auto invocation = std::make_shared(); + clang::CompilerInvocation::CreateFromArgs( + *invocation, + ::llvm::ArrayRef(vCompilerArgs.data(), vCompilerArgs.size()), + pOutInstance->getDiagnostics() + ); + + // Use C++20 + clang::LangStandard::Kind langKind; + auto& langOptions = invocation->getLangOpts(); + + langOptions.CPlusPlus = 1; + + switch (sCompilerConfig.cppStandard) + { + case CxxStandard::CC_11: + langOptions.CPlusPlus11 = 1; + langKind = clang::LangStandard::Kind::lang_cxx11; + break; + case CxxStandard::CC_14: + langOptions.CPlusPlus14 = 1; + langKind = clang::LangStandard::Kind::lang_cxx14; + break; + case CxxStandard::CC_17: + langOptions.CPlusPlus17 = 1; + langKind = clang::LangStandard::Kind::lang_cxx17; + break; + case CxxStandard::CC_20: + langOptions.CPlusPlus20 = 1; + langKind = clang::LangStandard::Kind::lang_cxx20; + break; + case CxxStandard::CC_23: + langOptions.CPlusPlus23 = 1; + langKind = clang::LangStandard::Kind::lang_cxx23; + break; + case CxxStandard::CC_26: + langOptions.CPlusPlus26 = 1; + langKind = clang::LangStandard::Kind::lang_cxx26; + break; + default: + langOptions.CPlusPlus11 = 1; + langKind = clang::LangStandard::Kind::lang_cxx11; + break; + } + + langOptions.LangStd = langKind; + langOptions.IsHeaderFile = true; // NOTE: Maybe we should use flag here? + +#ifdef _WIN32 + pOutInstance->getPreprocessorOpts().addMacroDef("_MSC_VER=1932"); + pOutInstance->getPreprocessorOpts().addMacroDef("_MSC_FULL_VER=193231329"); + pOutInstance->getPreprocessorOpts().addMacroDef("_MSC_EXTENSIONS"); + + /** + * Workaround: it's workaround for MSVC 2022 with yvals_core.h which send static assert when Clang version less than 17.x.x + * Example : static assertion failed: error STL1000: Unexpected compiler version, expected Clang 17.0.0 or newer. at C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.40.33807\include\yvals_core.h:898 + * + * This macro block static assertion and should help us, but this part of code MUST be removed after RG3 migrates to latest LLVM & Clang + */ + pOutInstance->getPreprocessorOpts().addMacroDef("_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH=1"); +#endif + +#ifdef __APPLE__ + // This should be enough? + pOutInstance->getPreprocessorOpts().addMacroDef("__GCC_HAVE_DWARF2_CFI_ASM=1"); +#endif + + std::shared_ptr targetOpts = nullptr; + + // Setup triple +#if !defined(__APPLE__) + if (pCompilerEnv->triple.empty()) + { + // Use default triple + targetOpts = std::make_shared(); + targetOpts->Triple = ::llvm::sys::getDefaultTargetTriple(); + clang::TargetInfo* targetInfo = clang::TargetInfo::CreateTargetInfo(pOutInstance->getDiagnostics(), targetOpts); + pOutInstance->setTarget(targetInfo); + + auto triple = targetInfo->getTriple(); + + std::vector vIncs; + clang::LangOptions::setLangDefaults(langOptions, clang::Language::CXX, triple, vIncs, langKind); + } + else + { + targetOpts = std::make_shared(); + targetOpts->Triple = ::llvm::Triple::normalize(pCompilerEnv->triple); + clang::TargetInfo* targetInfo = clang::TargetInfo::CreateTargetInfo(pOutInstance->getDiagnostics(), targetOpts); + pOutInstance->setTarget(targetInfo); + + auto triple = targetInfo->getTriple(); + + std::vector vIncs; + clang::LangOptions::setLangDefaults(langOptions, clang::Language::CXX, triple, vIncs, langKind); + } +#else + // On Apple we should use default triple instead of detect it at runtime + ::llvm::Triple triple(::llvm::sys::getDefaultTargetTriple()); + pOutInstance->getTargetOpts().Triple = triple.str(); + pOutInstance->setTarget(clang::TargetInfo::CreateTargetInfo(pOutInstance->getDiagnostics(), std::make_shared(pOutInstance->getTargetOpts()))); +#endif + + pOutInstance->setInvocation(invocation); + + // Set up FrontendOptions + clang::FrontendOptions &opts = pOutInstance->getFrontendOpts(); + opts.ProgramAction = clang::frontend::ParseSyntaxOnly; + opts.SkipFunctionBodies = static_cast(sCompilerConfig.bSkipFunctionBodies); + + opts.Inputs.clear(); + + // Prepare compiler instance + { + Visitor v { opts, pOutInstance }; + std::visit(v, sInput); + } + + // Set macros + clang::PreprocessorOptions& preprocessorOptions = pOutInstance->getPreprocessorOpts(); + for (const auto& compilerDef : sCompilerConfig.vCompilerDefs) + { + preprocessorOptions.addMacroDef(compilerDef); + } + + // Add builtins + preprocessorOptions.addMacroDef("__RG3__=1"); + preprocessorOptions.addMacroDef("__RG3_COMMIT__=\"" RG3_BUILD_HASH "\""); + preprocessorOptions.addMacroDef("__RG3_BUILD_DATE__=\"" __DATE__ "\""); + +#ifdef __APPLE__ + // For apple only. They cares about GNUC? Idk & I don't care + preprocessorOptions.addMacroDef("__GNUC__=4"); +#endif + + // Setup header dirs source + clang::HeaderSearchOptions& headerSearchOptions = pOutInstance->getHeaderSearchOpts(); + { + for (const auto& sysInc : pCompilerEnv->config.vSystemIncludes) + { + const auto absolutePath = std::filesystem::absolute(sysInc.sFsLocation); + + clang::frontend::IncludeDirGroup group = clang::frontend::IncludeDirGroup::Angled; + + if (sysInc.eKind == IncludeKind::IK_SYSROOT) + { + // ignore sysroot here + continue; + } + + if (sysInc.eKind == IncludeKind::IK_SYSTEM) + { + group = clang::frontend::IncludeDirGroup::System; + } + + if (sysInc.eKind == IncludeKind::IK_C_SYSTEM) + { + group = clang::frontend::IncludeDirGroup::ExternCSystem; + } + + headerSearchOptions.AddPath(absolutePath.string(), group, false, true); + } + + for (const auto& incInfo : sCompilerConfig.vIncludes) + { + // Convert path to absolute + const auto absolutePath = std::filesystem::absolute(incInfo.sFsLocation); + headerSearchOptions.AddPath(absolutePath.string(), clang::frontend::IncludeDirGroup::Angled, false, true); + } + } + + // small self check + assert(pOutInstance->hasDiagnostics() && "Diagnostics not set up!"); + assert(pOutInstance->hasTarget() && "Target not set up!"); + assert(pOutInstance->hasFileManager() && "FileManager not set up!"); + } +} \ No newline at end of file diff --git a/LLVM/source/Consumers/CollectConstexprVariableEvalResult.cpp b/LLVM/source/Consumers/CollectConstexprVariableEvalResult.cpp new file mode 100644 index 0000000..55cd2a7 --- /dev/null +++ b/LLVM/source/Consumers/CollectConstexprVariableEvalResult.cpp @@ -0,0 +1,73 @@ +#include + +#include +#include + + +namespace rg3::llvm::consumers +{ + class ConstexprVisitor : public clang::RecursiveASTVisitor { + private: + std::unordered_set m_aExpectedVariables {}; + std::unordered_map* m_pEvaluatedVariables { nullptr }; + clang::ASTContext& m_sContext; + + public: + explicit ConstexprVisitor(clang::ASTContext& context, std::unordered_map* pEvaluatedValues, const std::unordered_set& aExpectedVariables) + : m_sContext(context), m_aExpectedVariables(aExpectedVariables), m_pEvaluatedVariables(pEvaluatedValues) + { + } + + bool VisitVarDecl(clang::VarDecl* pVarDecl) + { + std::string sName = pVarDecl->getNameAsString(); + + if (pVarDecl->isConstexpr() && m_aExpectedVariables.contains(sName)) + { + auto* pEvaluated = pVarDecl->getEvaluatedValue(); + if (pEvaluated) + { + if (pVarDecl->getType()->isBooleanType()) + { + (*m_pEvaluatedVariables)[sName] = pEvaluated->getInt().getBoolValue(); + } + else if (pVarDecl->getType()->isSignedIntegerType()) + { + (*m_pEvaluatedVariables)[sName] = pEvaluated->getInt().getExtValue(); + } + else if (pVarDecl->getType()->isUnsignedIntegerType()) + { + (*m_pEvaluatedVariables)[sName] = pEvaluated->getInt().getZExtValue(); + } + else if (pVarDecl->getType()->isFloatingType()) + { + (*m_pEvaluatedVariables)[sName] = pEvaluated->getFloat().convertToFloat(); + } + else if (pVarDecl->getType()->isDoubleType()) + { + (*m_pEvaluatedVariables)[sName] = pEvaluated->getFloat().convertToDouble(); + } + else if (pVarDecl->getType()->isPointerType() && pVarDecl->getType()->getPointeeType()->isCharType()) + { + if (pEvaluated->isLValue()) + { + if (auto pStrValue = ::llvm::dyn_cast(pEvaluated->getLValueBase().get())) + { + (*m_pEvaluatedVariables)[sName] = pStrValue->getString().str(); + } + } + } + } + } + return true; // Continue traversal + } + }; + + CollectConstexprVariableEvalResult::CollectConstexprVariableEvalResult() = default; + + void CollectConstexprVariableEvalResult::HandleTranslationUnit(clang::ASTContext& ctx) + { + ConstexprVisitor visitor { ctx, pEvaluatedVariables, aExpectedVariables }; + visitor.TraverseDecl(ctx.getTranslationUnitDecl()); + } +} \ No newline at end of file diff --git a/PyBind/include/RG3/PyBind/PyAnalyzerContext.h b/PyBind/include/RG3/PyBind/PyAnalyzerContext.h index e081d97..7cde5ce 100644 --- a/PyBind/include/RG3/PyBind/PyAnalyzerContext.h +++ b/PyBind/include/RG3/PyBind/PyAnalyzerContext.h @@ -76,6 +76,7 @@ namespace rg3::pybind [[nodiscard]] const boost::python::list& getFoundIssues() const; [[nodiscard]] const boost::python::list& getFoundTypes() const; + [[nodiscard]] const rg3::llvm::CompilerConfig& getCompilerConfig() const { return m_compilerConfig; } public: /** diff --git a/PyBind/include/RG3/PyBind/PyCodeAnalyzerBuilder.h b/PyBind/include/RG3/PyBind/PyCodeAnalyzerBuilder.h index 8ee2079..2b61e35 100644 --- a/PyBind/include/RG3/PyBind/PyCodeAnalyzerBuilder.h +++ b/PyBind/include/RG3/PyBind/PyCodeAnalyzerBuilder.h @@ -38,6 +38,8 @@ namespace rg3::pybind const boost::python::list& getFoundTypes() const; const boost::python::list& getFoundIssues() const; + [[nodiscard]] const rg3::llvm::CompilerConfig& getCompilerConfig() const; + private: std::unique_ptr m_pAnalyzerInstance { nullptr }; boost::python::list m_foundTypes {}; diff --git a/PyBind/rg3py.pyi b/PyBind/rg3py.pyi index 18daa57..a1ebb17 100644 --- a/PyBind/rg3py.pyi +++ b/PyBind/rg3py.pyi @@ -2,7 +2,7 @@ This file contains all public available symbols & definitions for PyBind (rg3py.pyd) Follow PyBind/source/PyBind.cpp for details """ -from typing import List, Union, Optional +from typing import List, Union, Optional, Dict class CppStandard: @@ -358,6 +358,8 @@ class CodeAnalyzer: def analyze(self): ... + def make_evaluator(self) -> CodeEvaluator: ... + class AnalyzerContext: @staticmethod @@ -401,6 +403,23 @@ class AnalyzerContext: def analyze(self) -> bool: ... + def make_evaluator(self) -> CodeEvaluator: ... + + +class CodeEvaluator: + def __init__(self): ... + + def eval(self, code: str, capture: List[str]) -> Union[List[CppCompilerIssue], Dict[str, any]]: ... + + def set_cpp_standard(self, standard: CppStandard): ... + + def get_cpp_standard(self) -> CppStandard: ... + + def set_compiler_config(self, config: dict): ... + + @staticmethod + def make_from_system_env() -> CodeEvaluator|None: ... + class CppCompilerIssueKind: IK_NONE = 0 diff --git a/PyBind/source/PyBind.cpp b/PyBind/source/PyBind.cpp index 51683a8..a7a735a 100644 --- a/PyBind/source/PyBind.cpp +++ b/PyBind/source/PyBind.cpp @@ -12,6 +12,8 @@ #include #include +#include + #include #include #include @@ -20,7 +22,6 @@ #include - using namespace boost::python; @@ -163,6 +164,166 @@ namespace rg3::pybind::wrappers { return boost::python::str(sInfo.sPrettyName); } + + static boost::python::object CodeEvaluator_eval(rg3::llvm::CodeEvaluator& sEval, const std::string& sCode, const boost::python::list& aCapture) + { + std::vector aCaptureList {}; + aCaptureList.reserve(len(aCapture)); + + for (int i = 0; i < len(aCapture); ++i) + { + aCaptureList.push_back(boost::python::extract(aCapture[i])); + } + + // Invoke originals + auto sEvalResult = sEval.evaluateCode(sCode, aCaptureList); + if (!sEvalResult) + { + // Error! + boost::python::list aIssues {}; + + for (const auto& sIssue : sEvalResult.vIssues) + { + aIssues.append(sIssue); + } + + return aIssues; + } + + // Make dict + boost::python::dict result {}; + + for (const auto& [sKey, sValue] : sEvalResult.mOutputs) + { + std::visit([&result, &sKey](auto&& v) { + result[sKey] = v; + }, sValue); + } + + return result; + } + + static void CodeEvaluator_setCppStandard(rg3::llvm::CodeEvaluator& sEval, rg3::llvm::CxxStandard eStandard) + { + sEval.getCompilerConfig().cppStandard = eStandard; + } + + static void CodeEvaluator_setCompilerConfigFromDict(rg3::llvm::CodeEvaluator& sEval, const boost::python::object& sDescription) + { + /** + * Allowed fields: + * + * cpp_standard + * definitions + * allow_collect_non_runtime + * skip_function_bodies + * + * If description presented as dict - use directly, otherwise try cast to dict via __dict__ + */ + if (sDescription.is_none()) + { + PyErr_SetString(PyExc_MemoryError, "Expected to have a valid description instead of None"); + boost::python::throw_error_already_set(); + return; + } + + if (PyDict_Check(sDescription.ptr())) + { + // Use directly as dict + boost::python::dict pyDict = boost::python::extract(sDescription); + boost::python::list aKeys = pyDict.keys(); + + for (int i = 0; i < boost::python::len(aKeys); ++i) { + const std::string sKey = boost::python::extract(aKeys[i]); + + if (sKey == "cpp_standard") + { + sEval.getCompilerConfig().cppStandard = boost::python::extract(pyDict[sKey]); + } + + if (sKey == "definitions") + { + boost::python::list pyList = boost::python::extract(pyDict[sKey]); + + auto& definitions = sEval.getCompilerConfig().vCompilerDefs; + definitions.clear(); + definitions.reserve(boost::python::len(pyList)); + + for (auto j = 0; j < boost::python::len(pyList); ++j) { + definitions.push_back(boost::python::extract(pyList[j])); + } + } + + if (sKey == "allow_collect_non_runtime") + { + sEval.getCompilerConfig().bAllowCollectNonRuntimeTypes = boost::python::extract(pyDict[sKey]); + } + + if (sKey == "skip_function_bodies") + { + sEval.getCompilerConfig().bSkipFunctionBodies = boost::python::extract(pyDict[sKey]); + } + } + } + else + { + // Maybe subject able to cast with __dict__ ? + boost::python::object dictAttr = sDescription.attr("__dict__"); + if (PyDict_Check(dictAttr.ptr())) + { + // Use as dict + boost::python::dict pyDict = boost::python::extract(dictAttr); + + // Run self. NOTE: Maybe we've should check this for recursion? Idk) + CodeEvaluator_setCompilerConfigFromDict(sEval, pyDict); + } + else + { + // Omg, dict is not a dict. Raise our own error + PyErr_SetString(PyExc_TypeError, "Meta method __dict__ returned not a dict!"); + boost::python::throw_error_already_set(); + } + } + } + + static rg3::llvm::CxxStandard CodeEvaluator_getCppStandard(const rg3::llvm::CodeEvaluator& sEval) + { + return sEval.getCompilerConfig().cppStandard; + } + + static boost::shared_ptr CodeEvaluator_makeFromSystemEnv() + { + auto environmentExtractResult = rg3::llvm::CompilerConfigDetector::detectSystemCompilerEnvironment(); + if (rg3::llvm::CompilerEnvError* pError = std::get_if(&environmentExtractResult)) + { + std::string sErrorMessage = fmt::format("Failed to detect compiler environment: {}", pError->message); + + PyErr_SetString(PyExc_RuntimeError, sErrorMessage.c_str()); + boost::python::throw_error_already_set(); + return nullptr; + } + + auto pInstance = boost::shared_ptr(new rg3::llvm::CodeEvaluator()); + if (!pInstance) + { + PyErr_SetString(PyExc_MemoryError, "Out of memory (unable to allocate CodeEvaluator)"); + boost::python::throw_error_already_set(); + return nullptr; + } + + pInstance->setCompilerEnvironment(std::get(environmentExtractResult)); + return pInstance; + } + + static boost::shared_ptr PyAnalyzerContext_makeEvaluator(const rg3::pybind::PyAnalyzerContext& sContext) + { + return boost::shared_ptr(new rg3::llvm::CodeEvaluator(sContext.getCompilerConfig())); + } + + static boost::shared_ptr PyCodeAnalyzerBuilder_makeEvaluator(const rg3::pybind::PyCodeAnalyzerBuilder& sContext) + { + return boost::shared_ptr(new rg3::llvm::CodeEvaluator(sContext.getCompilerConfig())); + } } @@ -412,6 +573,7 @@ BOOST_PYTHON_MODULE(rg3py) .def("get_definitions", &rg3::pybind::PyCodeAnalyzerBuilder::getCompilerDefinitions) .def("set_definitions", &rg3::pybind::PyCodeAnalyzerBuilder::setCompilerDefinitions) .def("analyze", &rg3::pybind::PyCodeAnalyzerBuilder::analyze) + .def("make_evaluator", &rg3::pybind::wrappers::PyCodeAnalyzerBuilder_makeEvaluator) ; class_>("AnalyzerContext", "A multithreaded analyzer and scheduled which made to analyze a bunch of files at once. If you have more than few files you should use this class.", no_init) @@ -436,6 +598,7 @@ BOOST_PYTHON_MODULE(rg3py) .def("set_compiler_args", &rg3::pybind::PyAnalyzerContext::setCompilerArgs) .def("set_compiler_defs", &rg3::pybind::PyAnalyzerContext::setCompilerDefs) .def("analyze", &rg3::pybind::PyAnalyzerContext::analyze) + .def("make_evaluator", &rg3::pybind::wrappers::PyAnalyzerContext_makeEvaluator) // Resolvers .def("get_type_by_reference", &rg3::pybind::PyAnalyzerContext::pyGetTypeOfTypeReference) @@ -448,4 +611,14 @@ BOOST_PYTHON_MODULE(rg3py) .def("detect_system_include_sources", &rg3::pybind::PyClangRuntime::detectSystemIncludeSources) .staticmethod("detect_system_include_sources") ; + + class_>("CodeEvaluator", "Eval constexpr C++ code and provide access to result values", boost::python::init<>()) + .def("eval", &rg3::pybind::wrappers::CodeEvaluator_eval) + .def("set_cpp_standard", &rg3::pybind::wrappers::CodeEvaluator_setCppStandard) + .def("set_compiler_config", &rg3::pybind::wrappers::CodeEvaluator_setCompilerConfigFromDict) + .def("get_cpp_standard", &rg3::pybind::wrappers::CodeEvaluator_getCppStandard) + + .def("make_from_system_env", &rg3::pybind::wrappers::CodeEvaluator_makeFromSystemEnv) + .staticmethod("make_from_system_env") + ; } \ No newline at end of file diff --git a/PyBind/source/PyCodeAnalyzerBuilder.cpp b/PyBind/source/PyCodeAnalyzerBuilder.cpp index 437aeb1..75e9cf4 100644 --- a/PyBind/source/PyCodeAnalyzerBuilder.cpp +++ b/PyBind/source/PyCodeAnalyzerBuilder.cpp @@ -145,4 +145,9 @@ namespace rg3::pybind { return m_foundIssues; } + + const rg3::llvm::CompilerConfig& PyCodeAnalyzerBuilder::getCompilerConfig() const + { + return m_pAnalyzerInstance->getCompilerConfig(); + } } \ No newline at end of file diff --git a/README.md b/README.md index 08ec3ef..45f1e00 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Requirements Windows ------- - * Compiled LLVM (>= 18.1.8) on local machine and environment variables `CLANG_DIR` and `LLVM_DIR` + * Compiled LLVM (>= 19.1.4) on local machine and environment variables `CLANG_DIR` and `LLVM_DIR` * My `Clang_DIR` is `B:/Projects/llvm/build/lib/cmake/clang` * My `LLVM_DIR` is `B:/Projects/llvm/build/lib/cmake/llvm` * Statically compiled Boost (>=1.81.0) with boost python and environment variable `BOOST_ROOT` @@ -98,6 +98,42 @@ and output will be We have a type my::cool::name_space::ECoolEnum (TK_ENUM) ``` + +Another good use case is `constexpr` evaluation feature: +```python +import rg3py +from typing import Union, List, Dict + +evaluator: rg3py.CodeEvaluator = rg3py.CodeEvaluator.make_from_system_env() + +evaluator.set_compiler_config({ + "cpp_standard" : rg3py.CppStandard.CXX_20 +}) + +result: Union[Dict[str, any], List[rg3py.CppCompilerIssue]] = evaluator.eval(""" + #include + + class Simple {}; + class Base {}; + class Inherited : public Base {}; + + constexpr bool r0 = std::is_base_of_v; + constexpr bool r1 = std::is_base_of_v; + """, ["r0", "r1"]) # 1'st argument: code, 2'nd argument: constexpr captures (list of variables which will be exported from AST) + +print(result) # Result maybe a `dict` or `list`. When dict - result is good, when list - list of errors/issues presented +``` + +output will be + +```text +{'r1': False, 'r0': True} +``` + +and that's great feature to make some checks like `type should be inherited from A, have some methods and etc...` + +And this is independent of your current environment, only C++ STL library should be found! + Project state ------------- diff --git a/Tests/PyIntegration/tests.py b/Tests/PyIntegration/tests.py index b356d0e..bce7566 100644 --- a/Tests/PyIntegration/tests.py +++ b/Tests/PyIntegration/tests.py @@ -1,9 +1,21 @@ -from typing import Optional, List +from typing import Optional, List, Union, Dict +from dataclasses import dataclass import pytest import rg3py import os +@dataclass +class CompilerConfigDescription: + """ + Simple example of config + """ + cpp_standard: rg3py.CppStandard + definitions: List[str] + allow_collect_non_runtime: bool + skip_function_bodies: bool + + def test_code_analyzer_base(): analyzer: rg3py.CodeAnalyzer = rg3py.CodeAnalyzer.make() @@ -745,3 +757,150 @@ def test_check_move_and_copy_ctors_and_assign_operators(): assert analyzer.types[0].functions[1].owner == "Entity" assert analyzer.types[0].functions[1].is_noexcept is True assert analyzer.types[0].functions[1].is_static is False + +def test_code_eval_simple(): + evaluator: rg3py.CodeEvaluator = rg3py.CodeEvaluator() + result: Union[Dict[str, any], List[rg3py.CppCompilerIssue]] = evaluator.eval(""" + constexpr bool bIsOk = true; + constexpr float fIsOk = 1.337f; + constexpr const char* psOK = "OkayDude123"; + """, ["bIsOk", "fIsOk", "psOK"]) + + assert isinstance(result, dict) + assert "bIsOk" in result + assert "fIsOk" in result + assert "psOK" in result + + # With error + result = evaluator.eval("#error IDK", []) + assert isinstance(result, list) + assert len(result) == 1 + + issue: rg3py.CppCompilerIssue = result[0] + assert issue.message == 'IDK' + assert issue.kind == rg3py.CppCompilerIssueKind.IK_ERROR + assert issue.source_file == 'id0.hpp' + + +def test_code_eval_check_class_inheritance(): + analyzer: rg3py.CodeAnalyzer = rg3py.CodeAnalyzer.make() + analyzer.set_code("void do_foo() {}") + analyzer.set_cpp_standard(rg3py.CppStandard.CXX_20) + analyzer.analyze() + + evaluator: rg3py.CodeEvaluator = analyzer.make_evaluator() + result: Union[Dict[str, any], List[rg3py.CppCompilerIssue]] = evaluator.eval(""" + #include + + class Simple {}; + class Base {}; + class Inherited : public Base {}; + + constexpr bool r0 = std::is_base_of_v; + constexpr bool r1 = std::is_base_of_v; + """, ["r0", "r1"]) + + assert isinstance(result, dict) + assert result['r0'] is True + assert result['r1'] is False + + +def fib(x): + if x == 0: + return 0 + + if x == 1: + return 1 + + return fib(x - 1) + fib(x - 2) + + +def test_code_eval_check_fibonacci_in_constexpr(): + evaluator: rg3py.CodeEvaluator = rg3py.CodeEvaluator() + result: Union[Dict[str, any], List[rg3py.CppCompilerIssue]] = evaluator.eval(""" + constexpr int fibonacci(int n) + { + return n < 1 ? -1 : + (n == 1 || n == 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2)); + } + + constexpr auto f5 = fibonacci(5); + constexpr auto f10 = fibonacci(10); + """, ["f5", "f10"]) + + assert result['f5'] == fib(5) + assert result['f10'] == fib(10) + +def test_code_eval_check_build_instance_from_sys_env(): + evaluator: rg3py.CodeEvaluator = rg3py.CodeEvaluator.make_from_system_env() + assert evaluator is not None + + evaluator.set_compiler_config({ + "cpp_standard" : rg3py.CppStandard.CXX_20 + }) + + result: Union[Dict[str, any], List[rg3py.CppCompilerIssue]] = evaluator.eval(""" + #include + + class Simple {}; + class Base {}; + class Inherited : public Base {}; + + constexpr bool r0 = std::is_base_of_v; + constexpr bool r1 = std::is_base_of_v; + """, ["r0", "r1"]) + + assert isinstance(result, dict) + assert result['r0'] is True + assert result['r1'] is False + + +def test_code_check_batching_hardcore(): + evaluator: rg3py.CodeEvaluator = rg3py.CodeEvaluator.make_from_system_env() + assert evaluator is not None + + evaluator.set_cpp_standard(rg3py.CppStandard.CXX_20) + + code: str = """ + #include + + class Base {}; + """ + + for i in range(0, 100): + code = f"{code}\nclass Inherited{i} : Base {{}};\n" + + for i in range(0, 100): + code = f"{code}\nconstexpr bool bIsInherited{i} = std::is_base_of_v;\n" + + result: Union[Dict[str, any], List[rg3py.CppCompilerIssue]] = evaluator.eval(code, [f"bIsInherited{x}" for x in range(0, 100)]) + assert isinstance(result, dict) + assert all([result[f"bIsInherited{x}"] for x in range(0, 100)]) + +def test_code_eval_check_init_from_cfg(): + cfg: CompilerConfigDescription = CompilerConfigDescription(cpp_standard=rg3py.CppStandard.CXX_20, + definitions=[], + allow_collect_non_runtime=False, + skip_function_bodies=True) + + evaluator: rg3py.CodeEvaluator = rg3py.CodeEvaluator.make_from_system_env() + assert evaluator is not None + + assert evaluator.get_cpp_standard() != rg3py.CppStandard.CXX_20 + evaluator.set_compiler_config(cfg) + assert evaluator.get_cpp_standard() == rg3py.CppStandard.CXX_20 + + result: Union[Dict[str, any], List[rg3py.CppCompilerIssue]] = evaluator.eval(""" + #include + + class Simple {}; + class Base {}; + class Inherited : public Base {}; + + constexpr bool r0 = std::is_base_of_v; + constexpr bool r1 = std::is_base_of_v; + """, ["r0", "r1"]) + + assert isinstance(result, dict) + assert result['r0'] is True + assert result['r1'] is False diff --git a/Tests/Unit/source/Tests_CodeEvaluator.cpp b/Tests/Unit/source/Tests_CodeEvaluator.cpp new file mode 100644 index 0000000..908de92 --- /dev/null +++ b/Tests/Unit/source/Tests_CodeEvaluator.cpp @@ -0,0 +1,144 @@ +#include + +#include +#include + + +class Tests_CodeEvaluator : public ::testing::Test +{ + protected: + void SetUp() override + { + g_Eval = std::make_unique(); + g_Eval->getCompilerConfig().cppStandard = rg3::llvm::CxxStandard::CC_20; //23 std raise exceptions on macOS! + g_Eval->getCompilerConfig().bSkipFunctionBodies = true; + } + + void TearDown() override + { + g_Eval = nullptr; + } + + protected: + std::unique_ptr g_Eval { nullptr }; +}; + + +TEST_F(Tests_CodeEvaluator, SimpleUsage) +{ + auto res = g_Eval->evaluateCode("static constexpr int aResult = 32 * 2;", { "aResult" }); + ASSERT_TRUE(res.vIssues.empty()) << "No issues expected to be here"; + ASSERT_EQ(res.mOutputs.size(), 1) << "Expected to have 1 output"; + ASSERT_TRUE(res.mOutputs.contains("aResult")) << "aResult should be here"; + ASSERT_TRUE(std::holds_alternative(res.mOutputs["aResult"])); + ASSERT_EQ(std::get(res.mOutputs["aResult"]), 64) << "Must be 64!"; +} + +TEST_F(Tests_CodeEvaluator, ClassInheritanceConstexprTest) +{ + auto res = g_Eval->evaluateCode(R"( +#include + +struct MyCoolBaseClass {}; + +struct MyDniweClass {}; +struct MyBuddyClass : MyCoolBaseClass {}; + +constexpr bool g_bDniweInherited = std::is_base_of_v; +constexpr bool g_bBuddyInherited = std::is_base_of_v; +)", { "g_bDniweInherited", "g_bBuddyInherited" }); + + ASSERT_TRUE(res.vIssues.empty()) << "No issues expected to be here"; + ASSERT_EQ(res.mOutputs.size(), 2) << "Expected to have 2 outputs"; + ASSERT_TRUE(res.mOutputs.contains("g_bDniweInherited")) << "g_bDniweInherited should be here"; + ASSERT_TRUE(res.mOutputs.contains("g_bBuddyInherited")) << "g_bBuddyInherited should be here"; + ASSERT_TRUE(std::get(res.mOutputs["g_bBuddyInherited"])) << "Buddy should be ok!"; + ASSERT_FALSE(std::get(res.mOutputs["g_bDniweInherited"])) << "Dniwe should be bad!"; +} + +constexpr int fibonacci(int n) +{ + return n < 1 ? -1 : (n == 1 || n == 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2)); +} + +TEST_F(Tests_CodeEvaluator, FibonacciConstexprEvalTest) +{ + auto res = g_Eval->evaluateCode(R"( +constexpr int fibonacci(int n) +{ + return n < 1 ? -1 : (n == 1 || n == 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2)); +} + +constexpr auto g_iFib9 = fibonacci(9); +constexpr auto g_iFib10 = fibonacci(10); +constexpr auto g_iFib16 = fibonacci(16); +)", { "g_iFib9", "g_iFib10", "g_iFib16" }); + + ASSERT_TRUE(res.vIssues.empty()) << "No issues expected to be here"; + ASSERT_EQ(res.mOutputs.size(), 3) << "Expected to have 3 outputs"; + ASSERT_TRUE(res.mOutputs.contains("g_iFib9")) << "g_iFib9 should be here"; + ASSERT_TRUE(res.mOutputs.contains("g_iFib10")) << "g_iFib10 should be here"; + ASSERT_TRUE(res.mOutputs.contains("g_iFib16")) << "g_iFib16 should be here"; + ASSERT_EQ(std::get(res.mOutputs["g_iFib9"]), fibonacci(9)) << "FIB(9) FAILED"; + ASSERT_EQ(std::get(res.mOutputs["g_iFib10"]), fibonacci(10)) << "FIB(10) FAILED"; + ASSERT_EQ(std::get(res.mOutputs["g_iFib16"]), fibonacci(16)) << "FIB(16) FAILED"; +} + +TEST_F(Tests_CodeEvaluator, SampleString) +{ + auto res = g_Eval->evaluateCode(R"( +#ifdef __RG3_CODE_EVAL__ +constexpr const char* psFileID = __FILE__; +#else +# error "What the hell is going on here???" +#endif +)", { "psFileID" }); + + ASSERT_TRUE(res.vIssues.empty()) << "No issues expected to be here"; + ASSERT_TRUE(res.mOutputs.contains("psFileID")); + ASSERT_EQ(std::get(res.mOutputs["psFileID"]), "id0.hpp"); +} + +constexpr std::uint32_t FNV1aPrime = 16777619u; +constexpr std::uint32_t FNV1aOffsetBasis = 2166136261u; + +constexpr std::uint32_t fnv1aHash(const char* str, std::size_t length, std::uint32_t hash = FNV1aOffsetBasis) { + return (length == 0) + ? hash + : fnv1aHash(str + 1, length - 1, (hash ^ static_cast(*str)) * FNV1aPrime); +} + +constexpr std::uint32_t fnv1aHash(const char* str) { + std::size_t length = 0; + while (str[length] != '\0') ++length; + return fnv1aHash(str, length); +} + +TEST_F(Tests_CodeEvaluator, HashComputeExample) +{ + auto res = g_Eval->evaluateCode(R"( +#include +#include + +constexpr std::uint32_t FNV1aPrime = 16777619u; +constexpr std::uint32_t FNV1aOffsetBasis = 2166136261u; + +constexpr std::uint32_t fnv1aHash(const char* str, std::size_t length, std::uint32_t hash = FNV1aOffsetBasis) { + return (length == 0) + ? hash + : fnv1aHash(str + 1, length - 1, (hash ^ static_cast(*str)) * FNV1aPrime); +} + +constexpr std::uint32_t fnv1aHash(const char* str) { + std::size_t length = 0; + while (str[length] != '\0') ++length; + return fnv1aHash(str, length); +} + +constexpr const char* testString = "HelloWorldThisIsSamplePr0gr7mmForTe$tHashing"; +constexpr std::uint32_t testHash = fnv1aHash(testString); +)", { "testHash" }); + + ASSERT_TRUE(res.vIssues.empty()) << "No issues expected to be here"; + ASSERT_EQ(std::get(res.mOutputs["testHash"]), fnv1aHash("HelloWorldThisIsSamplePr0gr7mmForTe$tHashing")); +} \ No newline at end of file