Index: clang-tools-extra/clangd/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/CMakeLists.txt +++ clang-tools-extra/clangd/CMakeLists.txt @@ -97,6 +97,7 @@ IncludeFixer.cpp InlayHints.cpp JSONTransport.cpp + ModulesManager.cpp PathMapping.cpp Protocol.cpp Quality.cpp @@ -173,6 +174,7 @@ clangToolingInclusions clangToolingInclusionsStdlib clangToolingSyntax + clangDependencyScanning ) target_link_libraries(clangDaemon Index: clang-tools-extra/clangd/ClangdLSPServer.h =================================================================== --- clang-tools-extra/clangd/ClangdLSPServer.h +++ clang-tools-extra/clangd/ClangdLSPServer.h @@ -43,6 +43,8 @@ /// Look for compilation databases, rather than using compile commands /// set via LSP (extensions) only. bool UseDirBasedCDB = true; + /// Enable experimental support for modules. + bool ExperimentalModulesSupport = false; /// The offset-encoding to use, or std::nullopt to negotiate it over LSP. std::optional Encoding; /// If set, periodically called to release memory. Index: clang-tools-extra/clangd/ClangdLSPServer.cpp =================================================================== --- clang-tools-extra/clangd/ClangdLSPServer.cpp +++ clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -14,6 +14,7 @@ #include "Feature.h" #include "GlobalCompilationDatabase.h" #include "LSPBinder.h" +#include "ModulesManager.h" #include "Protocol.h" #include "SemanticHighlighting.h" #include "SourceCode.h" @@ -535,8 +536,14 @@ if (const auto &Dir = Params.initializationOptions.compilationDatabasePath) CDBOpts.CompileCommandsDir = Dir; CDBOpts.ContextProvider = Opts.ContextProvider; - BaseCDB = - std::make_unique(CDBOpts); + + if (Opts.ExperimentalModulesSupport) + BaseCDB = + std::make_unique( + CDBOpts, Opts.AsyncThreadsCount); + else + BaseCDB = + std::make_unique(CDBOpts); } auto Mangler = CommandMangler::detect(); Mangler.SystemIncludeExtractor = Index: clang-tools-extra/clangd/GlobalCompilationDatabase.h =================================================================== --- clang-tools-extra/clangd/GlobalCompilationDatabase.h +++ clang-tools-extra/clangd/GlobalCompilationDatabase.h @@ -31,6 +31,8 @@ std::string SourceRoot; }; +class ModulesManager; + /// Provides compilation arguments used for parsing C and C++ files. class GlobalCompilationDatabase { public: @@ -45,6 +47,10 @@ return std::nullopt; } + virtual std::vector getAllFilesInProjectOf(PathRef File) const { + return {}; + } + /// Makes a guess at how to build a file. /// The default implementation just runs clang on the file. /// Clangd should treat the results as unreliable. @@ -61,6 +67,8 @@ return OnCommandChanged.observe(std::move(L)); } + virtual ModulesManager *getModulesManager() const { return nullptr; } + protected: mutable CommandChanged OnCommandChanged; }; @@ -75,11 +83,15 @@ getCompileCommand(PathRef File) const override; std::optional getProjectInfo(PathRef File) const override; + virtual std::vector + getAllFilesInProjectOf(PathRef File) const override; tooling::CompileCommand getFallbackCommand(PathRef File) const override; bool blockUntilIdle(Deadline D) const override; + virtual ModulesManager *getModulesManager() const override; + private: const GlobalCompilationDatabase *Base; std::unique_ptr BaseOwner; @@ -121,10 +133,12 @@ /// Returns the path to first directory containing a compilation database in /// \p File's parents. std::optional getProjectInfo(PathRef File) const override; + virtual std::vector + getAllFilesInProjectOf(PathRef File) const override; bool blockUntilIdle(Deadline Timeout) const override; -private: +protected: Options Opts; class DirectoryCache; @@ -161,6 +175,40 @@ friend class DirectoryBasedGlobalCompilationDatabaseCacheTest; }; +/// DirectoryBasedModulesGlobalCompilationDatabase - owns the modules manager +/// and replace the module related options to refer to the modules built by +/// clangd it self. So that we can avoid depending the compiler in the user +/// space and avoid affecting user's build. +class DirectoryBasedModulesGlobalCompilationDatabase + : public DirectoryBasedGlobalCompilationDatabase { +public: + DirectoryBasedModulesGlobalCompilationDatabase( + const DirectoryBasedGlobalCompilationDatabase::Options &CDB, + unsigned AsyncThreadsCount); + + virtual ModulesManager *getModulesManager() const override { + return ModuleMgr.get(); + } + + virtual std::optional + getCompileCommand(PathRef File) const override; + + bool blockUntilIdle(Deadline D) const override; + + std::optional + getOriginalCompileCommand(PathRef File) const; + + const ThreadsafeFS &getThreadsafeFS() const { return Opts.TFS; } + +private: + llvm::SmallString<128> getModuleFilesCachePrefix(PathRef File) const; + llvm::SmallString<128> getMappedModuleFiles(PathRef) const; + + std::unique_ptr ModuleMgr; + + GlobalCompilationDatabase::CommandChanged::Subscription CommandsChanged; +}; + /// Extracts system include search path from drivers matching QueryDriverGlobs /// and adds them to the compile flags. /// Returns null when \p QueryDriverGlobs is empty. Index: clang-tools-extra/clangd/GlobalCompilationDatabase.cpp =================================================================== --- clang-tools-extra/clangd/GlobalCompilationDatabase.cpp +++ clang-tools-extra/clangd/GlobalCompilationDatabase.cpp @@ -9,6 +9,7 @@ #include "GlobalCompilationDatabase.h" #include "Config.h" #include "FS.h" +#include "ModulesManager.h" #include "SourceCode.h" #include "support/Logger.h" #include "support/Path.h" @@ -729,6 +730,22 @@ return Res->PI; } +std::vector +DirectoryBasedGlobalCompilationDatabase::getAllFilesInProjectOf( + PathRef File) const { + CDBLookupRequest Req; + Req.FileName = File; + Req.ShouldBroadcast = false; + Req.FreshTime = Req.FreshTimeMissing = + std::chrono::steady_clock::time_point::min(); + auto Res = lookupCDB(Req); + if (!Res) { + log("Failed to get the Compilation Database?"); + return {}; + } + return Res->CDB->getAllFiles(); +} + OverlayCDB::OverlayCDB(const GlobalCompilationDatabase *Base, std::vector FallbackFlags, CommandMangler Mangler) @@ -805,6 +822,13 @@ return Base->getProjectInfo(File); } +std::vector +DelegatingCDB::getAllFilesInProjectOf(PathRef File) const { + if (!Base) + return {}; + return Base->getAllFilesInProjectOf(File); +} + tooling::CompileCommand DelegatingCDB::getFallbackCommand(PathRef File) const { if (!Base) return GlobalCompilationDatabase::getFallbackCommand(File); @@ -817,5 +841,117 @@ return Base->blockUntilIdle(D); } +ModulesManager *DelegatingCDB::getModulesManager() const { + if (!Base) + return nullptr; + return Base->getModulesManager(); +} + +DirectoryBasedModulesGlobalCompilationDatabase:: + DirectoryBasedModulesGlobalCompilationDatabase( + const DirectoryBasedGlobalCompilationDatabase::Options &options, + unsigned AsyncThreadsCount) + : DirectoryBasedGlobalCompilationDatabase(options) { + ModuleMgr = std::make_unique(*this, AsyncThreadsCount); + + CommandsChanged = watch([ModuleMgr = ModuleMgr.get()]( + const std::vector &ChangedFiles) { + ModuleMgr->UpdateBunchFiles(ChangedFiles); + }); +} + +llvm::SmallString<128> +DirectoryBasedModulesGlobalCompilationDatabase::getModuleFilesCachePrefix( + PathRef File) const { + std::optional PI = getProjectInfo(File); + if (!PI) + return {}; + + llvm::SmallString<128> Result(PI->SourceRoot); + llvm::sys::path::append(Result, ".cache"); + llvm::sys::path::append(Result, "clangd"); + llvm::sys::path::append(Result, "module_files"); + + llvm::sys::fs::create_directories(Result, /*IgnoreExisting*/ true); + return Result; +} + +llvm::SmallString<128> +DirectoryBasedModulesGlobalCompilationDatabase::getMappedModuleFiles( + PathRef File) const { + std::string ModuleFileName = ModuleMgr->GetModuleInterfaceName(File); + + // Replace ":" in the module name with "-" to follow clang's style. Since ":" + // is not a valid character in some file systems. + auto [PrimaryName, PartitionName] = + llvm::StringRef(ModuleFileName).split(":"); + if (!PartitionName.empty()) + ModuleFileName = PrimaryName.str() + "-" + PartitionName.str(); + + llvm::SmallString<128> Result = getModuleFilesCachePrefix(File); + llvm::sys::path::append(Result, ModuleFileName + ".pcm"); + return Result; +} + +std::optional +DirectoryBasedModulesGlobalCompilationDatabase::getOriginalCompileCommand( + PathRef File) const { + return DirectoryBasedGlobalCompilationDatabase::getCompileCommand(File); +} + +bool DirectoryBasedModulesGlobalCompilationDatabase::blockUntilIdle( + Deadline D) const { + ModuleMgr->waitUntilInitialized(); + return DirectoryBasedGlobalCompilationDatabase::blockUntilIdle(D); +} + +/// Transfer the original command to relocate to the modules related +/// options. +/// 1. If there is no modules dependency graph or the required file +/// doesn't live in the graph, return the original command. +/// 2. If the file is a module interface unit with name +/// `module.name:partition.name`, change the output file to +/// `/<.cache>/clangd/module_files/module.name-partion.name.pcm`. +/// 3. Insert `-fprebuilt-module-path` to the front of the command line, +/// so that clangd can get the built-by-clangd BMIs first. +/// 4. Remove all the `-fmodule-file==` options. +std::optional +DirectoryBasedModulesGlobalCompilationDatabase::getCompileCommand( + PathRef File) const { + auto Result = + DirectoryBasedGlobalCompilationDatabase::getCompileCommand(File); + if (!Result) + return std::nullopt; + + if (!ModuleMgr->HasGraph() || !ModuleMgr->IsInGraph(File)) + return Result; + + if (ModuleMgr->IsModuleInterface(File)) + Result->Output = getMappedModuleFiles(File).str(); + + std::vector CommandLine; + CommandLine.reserve(Result->CommandLine.size() + 1); + + CommandLine.emplace_back(Result->CommandLine[0]); + llvm::SmallString<128> ModuleOutputPrefix = getModuleFilesCachePrefix(File); + CommandLine.emplace_back( + llvm::Twine("-fprebuilt-module-path=" + ModuleOutputPrefix).str()); + + for (std::size_t I = 1; I < Result->CommandLine.size(); I++) { + const std::string &Arg = Result->CommandLine[I]; + const auto &[Left, Right] = StringRef(Arg).split("="); + + // Remove original `-fmodule-file==` form. + if (Left == "-fmodule-file" && Right.contains("=")) + continue; + + CommandLine.emplace_back(Arg); + } + + Result->CommandLine = CommandLine; + + return Result; +} + } // namespace clangd } // namespace clang Index: clang-tools-extra/clangd/ModulesManager.h =================================================================== --- /dev/null +++ clang-tools-extra/clangd/ModulesManager.h @@ -0,0 +1,323 @@ +//===--- ModulesManager.h ----------------------------------------*- C++-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "GlobalCompilationDatabase.h" +#include "support/Path.h" +#include "support/Threading.h" + +#include "clang/Frontend/FrontendAction.h" +#include "clang/Tooling/DependencyScanning/DependencyScanningService.h" +#include "clang/Tooling/DependencyScanning/DependencyScanningTool.h" +#include "llvm/ADT/ConcurrentHashtable.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringSet.h" + +#include +#include +#include +#include +#include + +namespace clang::clangd { + +namespace detail { +/// A simple wrapper for AsyncTaskRunner and Semaphore to run modules related +/// task - Scanning, Generating Module Interfaces - concurrently. +class ModulesTaskRunner { + ModulesTaskRunner(unsigned AsyncThreadsCount) : Barrier(AsyncThreadsCount) { + if (AsyncThreadsCount > 0) + Runner.emplace(); + } + + friend class clang::clangd::ModulesManager; + +public: + void RunTask(llvm::unique_function Task, + const llvm::Twine &Name = ""); + + void wait(); + +private: + std::optional Runner; + Semaphore Barrier; +}; + +/// A data structure to describe the C++20 named modules dependency information +/// of the project. So that a file which doesn't declare a module name nor uses +/// a modules won't be recorded in the graph. +/// +/// Note that this graph is not thread safe. The thread safety is guaranteed by +/// its owner. +class ModulesDependencyGraph { + struct ModulesDependencyNode { + ModulesDependencyNode(PathRef Name) : Name(Name.str()) {} + + /// The corresponding filename of the node. + std::string Name; + /// The module unit name of the node (if it provides). + std::optional Provided; + /// The set of names that the node directly requires. The transitively + /// required names are not recorded here. + llvm::StringSet<> Requires; + + /// Update Provided and Requires information by provided P1689 rule. + /// + /// @return false if the node doesn't change. Return true otherwise. + bool + UpdateProvidedAndRequires(const tooling::dependencies::P1689Rule &Rule); + + /// The users of the current node. The node should be in the Deps for the + /// users. + std::set Users; + /// The dependent nodes of the current node. Note that it is possible that + /// `Requires.size() > Deps.size()` since there are possibly third party + /// modules for which we can't its source code. But the size of Deps should + /// never be larger than Requires. + std::set Deps; + + /// The corresponding BMI path of the current Node. This is only meaningful + /// if the current node is a module interface. + /// + /// When the BMIPath is not set, it implies that we know nothing about its + /// BMI. When one node has empty BMIPath (""), it implies that the node is + /// not able to be compiled. When one node has non-empty BMIPath, it implies + /// that the node has a meaningfull BMI and all the (transitively) dependent + /// nodes have meaningful BMI. + std::optional BMIPath; + }; + +public: + ModulesDependencyGraph() = default; + ModulesDependencyGraph(const ModulesDependencyGraph &) = delete; + ModulesDependencyGraph(ModulesDependencyGraph &&) = delete; + ModulesDependencyGraph operator=(const ModulesDependencyGraph &) = delete; + ModulesDependencyGraph operator=(ModulesDependencyGraph &&) = delete; + ~ModulesDependencyGraph() = default; + + bool empty() const { return Nodes.empty(); } + size_t size() const { return Nodes.size(); } + + bool IsInGraph(PathRef Path) const { return Nodes.count(Path.str()); } + bool IsModuleInterface(PathRef Path) const { + return IsInGraph(Path) && Nodes.at(Path.str())->Provided; + } + std::string GetModuleInterfaceName(PathRef Path) const { + assert(IsModuleInterface(Path)); + return *Nodes.at(Path.str())->Provided; + } + + /// Whether if all the (transitive) dependencies have valid BMI. + /// + /// Note that this doesn't include third party modules for which the source + /// codes can't be found. + bool IsReadyToCompile(PathRef Filename) const; + /// Whether there is any dependencies has invalid BMI. + bool HasInvalidDependencies(PathRef Filename) const; + + const ModulesDependencyNode *getNode(PathRef Path) const { + if (!Nodes.count(Path.str())) + return nullptr; + + return Nodes.at(Path.str()).get(); + } + + /// Get the direct users. + std::set getNodeUsers(PathRef Path) const { + if (!Nodes.count(Path.str())) + return {}; + + return Nodes.at(Path.str())->Users; + } + + /// Get all transitive users. + std::set getAllUsers(PathRef Path) const; + + void clear() { + Nodes.clear(); + ModuleMapper.clear(); + } + + bool SetBMIPath(PathRef Filename, PathRef BMIPath); + + /// Update/Insert a node in/to the graph. When all of \p AllowInsertNewNode , + /// \p AllowUpdateModuleMapper and \p AllowUpdateDependency are false, it is + /// thread safe to run `UpdateSingleNode` parallelly. If the caller want to + /// run `UpdateSingleNode` parallelly, it is the responsibility of the caller + /// to call `InsertNewNode` ahead of time and call `reconstructModuleMapper` + /// and `UpdateDependencies` later to maintain the validability of the graph. + void UpdateSingleNode(PathRef Filename, + const tooling::dependencies::P1689Rule &Rule, + bool AllowInsertNewNode = true, + bool AllowUpdateModuleMapper = true, + bool AllowUpdateDependency = true); + /// Insert new nodes to the graph. This shouldn't be called if the caller + /// don't want to run `UpdateSingleNode` parallelly. + void InsertNewNode(PathRef Filename); + /// Update the dependency information after updating node(s). This shouldn't + /// be called if the caller don't want to run `UpdateSingleNode` parallelly. + void UpdateDependencies(const llvm::StringSet<> &Files, + bool RecalculateEdges = true); + + /// Construct the module map from module name to ModulesDependencyNode* after + /// updating node(s). This shouldn't be called if the caller don't want to run + /// `UpdateSingleNode` parallelly. + void reconstructModuleMapper(); + + /// If the specified Path has (non-transitively) third party dependencies. + /// This is only called by testing purpose. + bool HasThirdpartyDependencies(PathRef Path) const; + +private: + void UpdateModuleMapper(PathRef Filename, + const tooling::dependencies::P1689Rule &Rule); + + /// Map from source file path to ModulesDependencyNode*. + llvm::StringMap> Nodes; + /// Map from the module name to ModulesDependencyNode*. This is a helper + /// strucutre to build the depenendcy graph faster. So it doesn't own the + /// nodes. It is OK for this to get ready before calling UpdateDependencies. + llvm::StringMap ModuleMapper; +}; +} // namespace detail + +/// A manager to manage the dependency information of modules and module files +/// states. The users can use `IsReadyToCompile(Path)` to get if it is Ok to +/// compile the file specified by `Path`. In case it is not ready, the user can +/// use the following pattern to wait for it to gets ready: +/// +/// ModuleMgr.addCallbackAfterReady(Path, [] () { +/// notify it is ready; +/// }); +/// ModuleMgr.GenerateModuleInterfacesFor(Path); +/// wait for it to get ready. +/// +/// Every time we meet a new file or a file get changed, we should call +/// `UpdateNode(PathRef)` to try to update its status in the graph no matter if +/// it was related to modules before. Since it is possible that the change of +/// the file is to introduce module related things. So it should be job of +/// ModulesManager to decide whether or not it is related to modules. +class ModulesManager { +public: + ModulesManager(const DirectoryBasedModulesGlobalCompilationDatabase &CDB, + unsigned AsyncThreadsCount); + ~ModulesManager(); + + /// If Filename is not related to moduls, e.g, not a module unit nor import + /// any modules, nothing will happen. Otherwise the modules manager will try + /// to update the modules graph responsitively. + void UpdateNode(PathRef Filename); + /// Update a lot of files at the same time. This should only be called by + /// DirectoryBasedModulesGlobalCompilationDatabase as a callback when finding + /// new compilation database. + void UpdateBunchFiles(const std::vector &Files); + + /// Whether we have atleast one node in the modules graph. + bool HasGraph() const; + /// If the specified file is in the graph. + bool IsInGraph(PathRef Path) const; + /// If the specified file is a module interface unit. + bool IsModuleInterface(PathRef Path) const; + std::string GetModuleInterfaceName(PathRef Path) const; + + /// Helper functions for testing + bool IsDirectlyDependent(PathRef A, PathRef B) const; + /// @return the number of nodes in the graph. + size_t GraphSize() const; + bool HasThirdpartyDependencies(PathRef A) const; + + /// We'll initialize the graph asynchronously. It is necessary for tests + /// to wait for the graph get initialized. + void waitUntilInitialized() const; + + /// @return If the BMIs of the dependencies for the specified file are valid + /// already. + bool IsReadyToCompile(PathRef Filename) const; + /// @return true if there is any (transitive) dependencies are invalid. False + /// otherwise, this doesn't include third party modules. + bool HasInvalidDependencies(PathRef Filename) const; + + /// Add a callback which will be called when the corresponding source file + /// gets ready. This requires `!IsReadyToCompile(Filename)` and + /// `!HasInvalidDependencies(Filename)`. + /// + /// @param ReadyCallback the callback get called when the file specified by + /// Filename gets ready or known not to be ready (since some dependencies + /// fail to compile). The callback should accept a boolean argument. This + /// boolean argument will be true when the file gets ready and false + /// otherwise. + void addCallbackAfterReady(PathRef Filename, + std::function ReadyCallback); + + /// Generate module files for file specified by Path. This will generate + /// module files for the transitively dependencies. It requires there is no + /// invalid dependencies. + void GenerateModuleInterfacesFor(PathRef Path); + +private: + void GenerateModuleInterface(PathRef Path); + + /// Invoke ClangScanDeps to get P1689 rule for the specified file. + std::optional + getRequiresAndProvide(PathRef Path); + + detail::ModulesDependencyGraph Graph; + + /// A mutex for the graph. Any operation to the graph should be guarded by the + /// mutex. + mutable std::mutex Mutex; + + /// A helper class to track the scheduled tasks to generate module files to + /// not schedule duplicated task to generate module files for the same file. + llvm::StringSet<> ScheduledInterfaces; + + /// A map from path of source files to the callbacks when the file gets ready + /// to compile. + llvm::StringMap, 8>> + WaitingCallables; + /// All the operations for WaitingCallables should be guarded by the mutex. + mutable std::mutex WaitingCallablesMutex; + /// Set BMIPath to the node specified by Filename and run the corresponding + /// callbacks (if any). If the BMIPath is empty, it implies that the node is + /// invalid to compile. + void setBMIPath(PathRef Filename, PathRef BMIPath); + + const DirectoryBasedModulesGlobalCompilationDatabase &CDB; + clang::tooling::dependencies::DependencyScanningService Service; + + enum GraphInitState { Uninitialized, Initializing, Initialized }; + std::atomic InitializeState = Uninitialized; + mutable std::mutex InitializationMutex; + mutable std::condition_variable InitCV; + + /// Atomatically test if the graph is uninitialized. And if yes, change its + /// value to Initializing atomatically. This should only be called before + /// InitializeGraph + bool IsUnitialized() { + unsigned UninitializedState = Uninitialized; + return InitializeState.compare_exchange_strong(UninitializedState, + Initializing); + } + + /// Lock the whole graph until is initialized. This is an abbrevation for: + /// + /// waitUntilInitialized(); + /// std::lock_guard Lock(Mutex); + [[nodiscard]] std::lock_guard lockGraph() const { + waitUntilInitialized(); + return std::lock_guard(Mutex); + } + + /// Initialize the graph. Modules manager will try to do it asynchronously if + /// we have the resources. + void InitializeGraph(PathRef Filename); + + unsigned AsyncThreadsCount = 0; + detail::ModulesTaskRunner Runner; +}; + +} // namespace clang::clangd Index: clang-tools-extra/clangd/ModulesManager.cpp =================================================================== --- /dev/null +++ clang-tools-extra/clangd/ModulesManager.cpp @@ -0,0 +1,637 @@ +//===--- ModulesManager.cpp --------------------------------------*- C++-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "ModulesManager.h" +#include "Compiler.h" +#include "support/Logger.h" + +#include "clang/Frontend/FrontendActions.h" +#include "clang/Tooling/Tooling.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/ScopeExit.h" + +using namespace clang; +using namespace clang::clangd; +using namespace clang::tooling::dependencies; + +ModulesManager::ModulesManager( + const DirectoryBasedModulesGlobalCompilationDatabase &CDB, + unsigned AsyncThreadsCount) + : CDB(CDB), Service(ScanningMode::CanonicalPreprocessing, + ScanningOutputFormat::P1689), + AsyncThreadsCount(AsyncThreadsCount), Runner(AsyncThreadsCount) {} + +ModulesManager::~ModulesManager() { Runner.wait(); } + +bool clang::clangd::detail::ModulesDependencyGraph::SetBMIPath( + PathRef Filename, PathRef BMIPath) { + if (!Nodes.count(Filename)) + return false; + + Nodes[Filename]->BMIPath = BMIPath; + return true; +} + +std::optional ModulesManager::getRequiresAndProvide(PathRef Name) { + std::optional Cmd = + CDB.getOriginalCompileCommand(Name); + + if (!Cmd) + return std::nullopt; + + using namespace clang::tooling::dependencies; + + DependencyScanningTool ScanningTool(Service); + + std::string MakeformatOutput; + std::string MakeformatOutputPath; + llvm::Expected P1689Rule = + ScanningTool.getP1689ModuleDependencyFile( + *Cmd, Cmd->Directory, MakeformatOutput, MakeformatOutputPath); + + if (!P1689Rule) + return std::nullopt; + + return *P1689Rule; +} + +void ModulesManager::UpdateBunchFiles(const std::vector &Files) { + if (IsUnitialized()) + return InitializeGraph(Files.front()); + + waitUntilInitialized(); + + // If the size of new files are not so many, maybe it is better + // to not re-initialize the whole graph. + // + // Maybe we need a better fine-tuned condition. + if (Files.size() < Graph.size() / 2) { + for (const std::string &File : Files) + UpdateNode(File); + + return; + } + + { + std::lock_guard Lock(Mutex); + Graph.clear(); + InitializeState = Initializing; + } + InitializeGraph(Files.front()); +} + +void ModulesManager::UpdateNode(PathRef Filename) { + std::optional Rule = getRequiresAndProvide(Filename); + if (!Rule) + return; + + auto _ = lockGraph(); + Graph.UpdateSingleNode(Filename, *Rule); +} + +void ModulesManager::waitUntilInitialized() const { + if (InitializeState == Initializing) { + std::unique_lock Lock(InitializationMutex); + InitCV.wait(Lock, [this]() { return InitializeState == Initialized; }); + } +} + +// TODO: Try to use the existing BMIs, which is created in last invocation of +// clangd. +void ModulesManager::InitializeGraph(PathRef Filename) { +#ifndef NDEBUG + assert(Graph.empty()); + assert(InitializeState == Initializing && + "InitializeState should be set before calling InitializeGraph."); +#endif + + // Initialization may be slow. Consider to do it asynchronously. + auto Task = [this, &CDB = CDB, &Graph = Graph, &Mutex = Mutex, + Filename = Filename.str(), &Runner = Runner]() { + std::vector AllFiles = CDB.getAllFilesInProjectOf(Filename); + llvm::StringSet<> ModuleRelatedFiles; + + // In case we don't have enough resources, run it locally. + // Note that the current task may use async resources. + if (AsyncThreadsCount <= 1) { + for (const std::string &File : AllFiles) { + std::optional Rule = getRequiresAndProvide(File); + if (!Rule) + continue; + + std::lock_guard Lock(Mutex); + ModuleRelatedFiles.insert(File); + Graph.UpdateSingleNode(File, *Rule, + /*InsertNewNode*/ true, + /*AllowUpdateModuleMapper*/ false, + /*UpdateDependency*/ false); + } + } else { + // We have threads. Let's try to scanning the graph pararrelly. + std::atomic UnfinishedTaskNum = AllFiles.size(); + std::mutex FinishedMu; + std::condition_variable FinishedCV; + + std::mutex Mu; + for (const auto &File : AllFiles) + Runner.RunTask([this, &Graph, File, &ModuleRelatedFiles, &FinishedCV, + &UnfinishedTaskNum, &Mu]() mutable { + std::optional Rule = getRequiresAndProvide(File); + if (!Rule) + return; + + // Try to make the critical section as short as possible. + { + std::lock_guard Lock(Mu); + Graph.InsertNewNode(File); + ModuleRelatedFiles.insert(File); + } + + // It is thread safe now by design. + Graph.UpdateSingleNode(File, *Rule, + /*InsertNewNode*/ false, + /*AllowUpdateModuleMapper*/ false, + /*UpdateDependency*/ false); + + UnfinishedTaskNum--; + + if (!UnfinishedTaskNum) + FinishedCV.notify_all(); + }); + + std::unique_lock Lock(FinishedMu); + FinishedCV.wait( + Lock, [&UnfinishedTaskNum]() { return UnfinishedTaskNum == 0; }); + } + + std::lock_guard Lock(Mutex); + // Now construct the module mapper in one go. + Graph.reconstructModuleMapper(); + Graph.UpdateDependencies(ModuleRelatedFiles); + InitializeState = Initialized; + InitCV.notify_all(); + }; + + Runner.RunTask(std::move(Task)); +} + +void clang::clangd::detail::ModulesDependencyGraph::reconstructModuleMapper() { + ModuleMapper.clear(); + + for (auto &&Key : Nodes.keys()) + if (Nodes[Key]->Provided) + ModuleMapper[*Nodes[Key]->Provided] = Nodes[Key].get(); +} + +bool clang::clangd::detail::ModulesDependencyGraph::ModulesDependencyNode:: + UpdateProvidedAndRequires(const P1689Rule &Rule) { + bool DependencyChanged = + Rule.Provides ? (Provided != Rule.Provides->ModuleName) : !Provided; + + if (DependencyChanged) { + if (Rule.Provides) + Provided = Rule.Provides->ModuleName; + else + Provided = std::nullopt; + } + + DependencyChanged |= (Requires.size() != Rule.Requires.size()); + llvm::StringSet<> RequiresTmp = Requires; + for (const P1689ModuleInfo &Required : Rule.Requires) { + auto [_, Inserted] = Requires.insert(Required.ModuleName); + DependencyChanged |= Inserted; + RequiresTmp.erase(Required.ModuleName); + } + + DependencyChanged |= !RequiresTmp.empty(); + for (auto &NotEliminated : RequiresTmp) + if (Requires.count(NotEliminated.getKey())) + Requires.erase(NotEliminated.getKey()); + + return DependencyChanged; +} + +void clang::clangd::detail::ModulesDependencyGraph::InsertNewNode( + PathRef Filename) { + if (!Nodes.count(Filename)) + Nodes.insert({Filename, std::make_unique(Filename)}); +} + +void clang::clangd::detail::ModulesDependencyGraph::UpdateModuleMapper( + PathRef Filename, const P1689Rule &Rule) { + ModulesDependencyNode *Node = Nodes[Filename].get(); + assert(Node); + + bool ModuleNameChanged = Rule.Provides + ? (Node->Provided != Rule.Provides->ModuleName) + : !Node->Provided; + + if (ModuleNameChanged) { + for (auto *User : Node->Users) + User->Deps.erase(Node); + + Node->Users.clear(); + } + + if (Node->Provided) { + llvm::StringRef OriginalModuleName = *Node->Provided; + assert(ModuleMapper.count(OriginalModuleName)); + ModuleMapper.erase(OriginalModuleName); + } + + if (!Rule.Provides) + return; + + llvm::StringRef NewModuleName = Rule.Provides->ModuleName; + ModuleMapper.insert({NewModuleName, Node}); +} + +void clang::clangd::detail::ModulesDependencyGraph::UpdateSingleNode( + PathRef Filename, const P1689Rule &Rule, bool AllowInsertNewNode, + bool AllowUpdateModuleMapper, bool UpdateDependency) { + if (AllowInsertNewNode) + InsertNewNode(Filename); + + if (AllowUpdateModuleMapper) + UpdateModuleMapper(Filename, Rule); + + ModulesDependencyNode *UpdatingNode = Nodes[Filename].get(); + assert(UpdatingNode); + bool DependencyChanged = UpdatingNode->UpdateProvidedAndRequires(Rule); + if (!UpdateDependency) + return; + + UpdateDependencies({Filename}, DependencyChanged); +} + +/// UpdateDependencies does 2 things: +/// - Add edges to the dependency graph. +/// - For all the changed files and the (transitively) users, reset their state +/// of BMIPath. +void clang::clangd::detail::ModulesDependencyGraph::UpdateDependencies( + const llvm::StringSet<> &Files, bool RecalculateEdges) { + if (RecalculateEdges) + for (const auto &FileIter : Files) { + llvm::StringRef File = FileIter.getKey(); + ModulesDependencyNode *Node = Nodes[File].get(); + assert(Node); + + for (auto *Dep : Node->Deps) + Dep->Users.erase(Node); + + Node->Deps.clear(); + + for (auto &Required : Node->Requires) { + llvm::StringRef RequiredModuleName = Required.getKey(); + + // It is possible that we're importing a third party module. + if (!ModuleMapper.count(RequiredModuleName)) + continue; + + ModulesDependencyNode *RequiredNode = ModuleMapper[RequiredModuleName]; + Node->Deps.insert(RequiredNode); + RequiredNode->Users.insert(Node); + } + } + + /// Reset the states of BMIPath. + llvm::SmallSetVector Worklist; + + // Due to the defect in llvm::StringSet, we can't use + // + // Worklist(Files.begin(), Files.end()); + // + // to initialize the worklist. + for (const auto &FileIter : Files) + Worklist.insert(FileIter.getKey()); + + // There shouldn't be circular modules dependency informations in a valid + // project. But we can't assume the project is valid. + llvm::StringSet<> NoDepCircleChecker; + + while (!Worklist.empty()) { + llvm::StringRef Filename = Worklist.pop_back_val(); + Nodes[Filename.str()]->BMIPath.reset(); + + if (NoDepCircleChecker.count(Filename)) { + elog("found circular modules dependency information in the project."); + break; + } + + NoDepCircleChecker.insert(Filename); + + for (auto *User : Nodes[Filename.str()]->Users) + if (Nodes[User->Name]->BMIPath) + Worklist.insert(User->Name); + } +} + +bool ModulesManager::HasGraph() const { + auto _ = lockGraph(); + return !Graph.empty(); +} + +bool ModulesManager::IsInGraph(PathRef Path) const { + auto _ = lockGraph(); + return Graph.IsInGraph(Path); +} + +bool ModulesManager::IsModuleInterface(PathRef Path) const { + auto _ = lockGraph(); + return Graph.IsModuleInterface(Path); +} + +std::string ModulesManager::GetModuleInterfaceName(PathRef Path) const { + auto _ = lockGraph(); + return Graph.GetModuleInterfaceName(Path); +} + +bool ModulesManager::IsReadyToCompile(PathRef Filename) const { + auto _ = lockGraph(); + return Graph.IsReadyToCompile(Filename); +} + +bool clang::clangd::detail::ModulesDependencyGraph::IsReadyToCompile( + PathRef Filename) const { + if (!Nodes.count(Filename)) + return true; + + auto *Node = Nodes.at(Filename).get(); + return llvm::all_of(Node->Deps, [](auto *Dep) { + return Dep->BMIPath && !Dep->BMIPath->empty(); + }); +} + +bool ModulesManager::HasInvalidDependencies(PathRef Filename) const { + auto _ = lockGraph(); + return Graph.HasInvalidDependencies(Filename); +} + +bool clang::clangd::detail::ModulesDependencyGraph::HasInvalidDependencies( + PathRef Filename) const { + if (!Nodes.count(Filename)) + return true; + + auto *Node = Nodes.at(Filename).get(); + // It is ok to lookup for the direct dependencies since once a node has + // invalid BMI, all of its users will have invalid BMI. + return llvm::any_of(Node->Deps, [](auto *Dep) { + return Dep->BMIPath && Dep->BMIPath->empty(); + }); +} + +namespace { +llvm::SmallString<128> getAbsolutePath(const tooling::CompileCommand &Cmd) { + llvm::SmallString<128> AbsolutePath; + if (llvm::sys::path::is_absolute(Cmd.Filename)) { + AbsolutePath = Cmd.Filename; + } else { + AbsolutePath = Cmd.Directory; + llvm::sys::path::append(AbsolutePath, Cmd.Filename); + llvm::sys::path::remove_dots(AbsolutePath, true); + } + return AbsolutePath; +} +} // namespace + +void ModulesManager::addCallbackAfterReady( + PathRef Filename, std::function ReadyCallback) { + std::lock_guard Lock(WaitingCallablesMutex); + assert(!IsReadyToCompile(Filename)); + assert(!HasInvalidDependencies(Filename)); + WaitingCallables[Filename].push_back(std::move(ReadyCallback)); +} + +std::set +clang::clangd::detail::ModulesDependencyGraph::getAllUsers( + PathRef Filename) const { + std::set Result; + + llvm::SmallSetVector Worklist; + Worklist.insert(Nodes.at(Filename).get()); + + while (!Worklist.empty()) { + auto *Node = Worklist.pop_back_val(); + assert(!Result.count(Node)); + Result.insert(Node); + + for (auto *User : Node->Users) + if (!Result.count(User)) + Worklist.insert(User); + } + + return Result; +} + +void ModulesManager::setBMIPath(PathRef Filename, PathRef BMIPath) { + auto _ = lockGraph(); + Graph.SetBMIPath(Filename, BMIPath); + bool Success = !BMIPath.empty(); + + for (auto *User : + Success ? Graph.getNodeUsers(Filename) : Graph.getAllUsers(Filename)) { + if (Success && !Graph.IsReadyToCompile(User->Name)) + continue; + + if (!Success) + Graph.SetBMIPath(User->Name, ""); + + std::lock_guard Lock(WaitingCallablesMutex); + if (!WaitingCallables.count(User->Name)) + continue; + + for (const auto &Task : WaitingCallables[User->Name]) + Runner.RunTask( + [Task = std::move(Task), Successed = Success]() { Task(Successed); }); + + WaitingCallables[User->Name].clear(); + } +} + +void ModulesManager::GenerateModuleInterface(PathRef Path) { + /// Return an empty string implies that a compilation failure. + /// Return a string to the compiled module interface file. + auto GenerateInterfaceTask = [this](PathRef Path) -> std::string { + auto _ = llvm::make_scope_exit([this, Path] { + auto _ = lockGraph(); + assert(ScheduledInterfaces.count(Path)); + ScheduledInterfaces.erase(Path); + }); + + auto Cmd = CDB.getCompileCommand(tooling::getAbsolutePath(Path)); + if (!Cmd) { + log("Failed to get the command for {0} when generating module interface", + Path); + return ""; + } + + ParseInputs Inputs; + Inputs.TFS = &CDB.getThreadsafeFS(); + Inputs.CompileCommand = std::move(*Cmd); + + IgnoreDiagnostics IgnoreDiags; + auto CI = buildCompilerInvocation(Inputs, IgnoreDiags); + if (!CI) { + log("Failed to build the compiler invocation for {0} when generating " + "module interface", + Path); + return ""; + } + + auto FS = Inputs.TFS->view(Inputs.CompileCommand.Directory); + auto AbsolutePath = getAbsolutePath(Inputs.CompileCommand); + auto Buf = FS->getBufferForFile(AbsolutePath); + if (!Buf) + return ""; + + log("Generating module file: {0}", Inputs.CompileCommand.Output); + CI->getFrontendOpts().OutputFile = Inputs.CompileCommand.Output; + + auto Clang = + prepareCompilerInstance(std::move(CI), /*Preamble=*/nullptr, + std::move(*Buf), std::move(FS), IgnoreDiags); + if (!Clang) + return ""; + + GenerateModuleInterfaceAction Action; + Clang->ExecuteAction(Action); + + if (Clang->getDiagnostics().hasErrorOccurred()) + return ""; + + return Inputs.CompileCommand.Output; + }; + + auto Task = [Path = Path.str(), this, + GenerateInterfaceTask = std::move(GenerateInterfaceTask)] { + { + auto _ = lockGraph(); + assert(Graph.IsModuleInterface(Path)); + if (ScheduledInterfaces.count(Path)) + return; + + ScheduledInterfaces.insert(Path); + } + + setBMIPath(Path, GenerateInterfaceTask(Path)); + }; + + assert(!HasInvalidDependencies(Path) && + "It is meaningless to require to generate module interface if there " + "is invalid dependencies."); + + if (!IsReadyToCompile(Path)) { + addCallbackAfterReady(Path, [Task = std::move(Task)](bool Ready) { + if (Ready) + Task(); + }); + GenerateModuleInterfacesFor(Path); + return; + } + + Runner.RunTask(std::move(Task)); +} + +void ModulesManager::GenerateModuleInterfacesFor(PathRef Path) { + // No graph implies that we're not in a modules repo. + if (!Graph.IsInGraph(Path)) + return; + + assert(!HasInvalidDependencies(Path) && + "It is meaningless to require to generate module interface if there " + "is invalid dependencies."); + + if (Graph.IsReadyToCompile(Path)) + return; + + llvm::SmallVector Names; + { + auto _ = lockGraph(); + for (auto *Dep : Graph.getNode(Path)->Deps) + if (!Dep->BMIPath) + Names.push_back(Dep->Name); + } + + for (const auto &Name : Names) + GenerateModuleInterface(Name); + + return; +} + +bool ModulesManager::IsDirectlyDependent(PathRef A, PathRef B) const { + auto _ = lockGraph(); + + auto *NodeA = Graph.getNode(A); + auto *NodeB = Graph.getNode(B); + + assert(NodeA); + assert(NodeB); + assert(NodeB->Provided); + + using namespace llvm; + return any_of(NodeA->Deps, [NodeA, NodeB](auto *Dep) { + // This is a little bit redudandant due to this is for testing. + return Dep == NodeB && + any_of(NodeB->Users, [NodeA](auto *User) { return User == NodeA; }); + }); +} + +size_t ModulesManager::GraphSize() const { + auto _ = lockGraph(); + return Graph.size(); +} + +bool clang::clangd::detail::ModulesDependencyGraph::HasThirdpartyDependencies( + PathRef Path) const { + auto *Node = getNode(Path); + + assert(Node); + assert(Node->Requires.size() >= Node->Users.size()); + + // This is redundant intentionally due to this function should be called + // in test. + bool Result = false; + for (const auto &Required : Node->Requires) { + llvm::StringRef RequiredModuleName = Required.getKey(); + + if (!ModuleMapper.count(RequiredModuleName)) { + Result = true; + continue; + } + + // Test that the required node is in the deps. + auto *RequiredNode = ModuleMapper.at(RequiredModuleName); + assert(llvm::any_of( + Node->Deps, [RequiredNode](auto *Dep) { return RequiredNode == Dep; })); + } + + return Result; +} + +bool ModulesManager::HasThirdpartyDependencies(PathRef Path) const { + auto _ = lockGraph(); + return Graph.HasThirdpartyDependencies(Path); +} + +void clang::clangd::detail::ModulesTaskRunner::RunTask( + llvm::unique_function Task, const llvm::Twine &Name) { + if (Runner) + Runner->runAsync( + Name, [Task = std::move(Task), &Barrier = this->Barrier]() mutable { + std::unique_lock Lock(Barrier, std::try_to_lock); + Task(); + }); + else + Task(); +} + +void clang::clangd::detail::ModulesTaskRunner::wait() { + if (Runner) + Runner->wait(); +} Index: clang-tools-extra/clangd/TUScheduler.cpp =================================================================== --- clang-tools-extra/clangd/TUScheduler.cpp +++ clang-tools-extra/clangd/TUScheduler.cpp @@ -52,6 +52,7 @@ #include "Config.h" #include "Diagnostics.h" #include "GlobalCompilationDatabase.h" +#include "ModulesManager.h" #include "ParsedAST.h" #include "Preamble.h" #include "index/CanonicalIncludes.h" @@ -647,6 +648,9 @@ TUScheduler::FileStats stats() const; bool isASTCached() const; + void waitForModulesBuilt() const; + void notifyModulesBuilt() const; + private: // Details of an update request that are relevant to scheduling. struct UpdateType { @@ -758,6 +762,10 @@ SynchronizedTUStatus Status; PreambleThread PreamblePeer; + + mutable std::condition_variable ModulesCV; + // mutable std::condition_variable FileInputsCV; + // std::mutex InitMu; }; /// A smart-pointer-like class that points to an active ASTWorker. @@ -855,6 +863,17 @@ bool ContentChanged) { llvm::StringLiteral TaskName = "Update"; auto Task = [=]() mutable { + if (auto *ModuleMgr = CDB.getModulesManager(); + ModuleMgr && !ModuleMgr->IsReadyToCompile(FileName) && + !ModuleMgr->HasInvalidDependencies(FileName)) { + log("{0} is not ready. wait for all the modules built.", FileName); + + ModuleMgr->addCallbackAfterReady(FileName, + [this](bool) { notifyModulesBuilt(); }); + ModuleMgr->GenerateModuleInterfacesFor(FileName); + waitForModulesBuilt(); + } + // Get the actual command as `Inputs` does not have a command. // FIXME: some build systems like Bazel will take time to preparing // environment to build the file, it would be nice if we could emit a @@ -1278,6 +1297,18 @@ PreambleCV.wait(Lock, [this] { return LatestPreamble || Done; }); } +void ASTWorker::waitForModulesBuilt() const { + auto *ModuleMgr = CDB.getModulesManager(); + assert(ModuleMgr); + std::unique_lock Lock(Mutex); + ModulesCV.wait(Lock, [ModuleMgr, this]() { + return ModuleMgr->IsReadyToCompile(FileName) || + ModuleMgr->HasInvalidDependencies(FileName); + }); +} + +void ASTWorker::notifyModulesBuilt() const { ModulesCV.notify_all(); } + tooling::CompileCommand ASTWorker::getCurrentCompileCommand() const { std::unique_lock Lock(Mutex); return FileInputs.CompileCommand; @@ -1684,7 +1715,13 @@ ContentChanged = true; FD->Contents = Inputs.Contents; } + + if (auto *ModuleMgr = CDB.getModulesManager()) + // TODO: Update all the nodes which are dependent on this. + ModuleMgr->UpdateNode(File.str()); + FD->Worker->update(std::move(Inputs), WantDiags, ContentChanged); + // There might be synthetic update requests, don't change the LastActiveFile // in such cases. if (ContentChanged) Index: clang-tools-extra/clangd/test/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/test/CMakeLists.txt +++ clang-tools-extra/clangd/test/CMakeLists.txt @@ -4,6 +4,7 @@ clangd-indexer # No tests for it, but we should still make sure they build. dexp + split-file ) if(CLANGD_BUILD_XPC) Index: clang-tools-extra/clangd/test/modules.test =================================================================== --- /dev/null +++ clang-tools-extra/clangd/test/modules.test @@ -0,0 +1,79 @@ +# A smoke test to check the modules can work basically. +# +# RUN: rm -fr %t +# RUN: mkdir -p %t +# RUN: split-file %s %t +# +# RUN: sed -e "s|DIR|%/t|g" %t/compile_commands.json.tmpl > %t/compile_commands.json.tmp +# RUN: sed -e "s|CLANG_CC|%clang|g" %t/compile_commands.json.tmp > %t/compile_commands.json +# RUN: sed -e "s|DIR|%/t|g" %t/definition.jsonrpc.tmpl > %t/definition.jsonrpc +# +# RUN: clangd -experimental-modules-support -lit-test < %t/definition.jsonrpc \ +# RUN: | FileCheck -strict-whitespace %t/definition.jsonrpc + +#--- A.cppm +export module A; +export void printA() {} + +#--- Use.cpp +import A; +void foo() { + print +} + +#--- compile_commands.json.tmpl +[ + { + "directory": "DIR", + "command": "CLANG_CC -fprebuilt-module-path=DIR -std=c++20 -o DIR/main.cpp.o -c DIR/Use.cpp", + "file": "DIR/Use.cpp" + }, + { + "directory": "DIR", + "command": "CLANG_CC -std=c++20 DIR/A.cppm --precompile -o DIR/A.pcm", + "file": "DIR/A.cppm" + } +] + +#--- definition.jsonrpc.tmpl +{ + "jsonrpc": "2.0", + "id": 0, + "method": "initialize", + "params": { + "processId": 123, + "rootPath": "clangd", + "capabilities": { + "textDocument": { + "completion": { + "completionItem": { + "snippetSupport": true + } + } + } + }, + "trace": "off" + } +} +--- +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file://DIR/Use.cpp", + "languageId": "cpp", + "version": 1, + "text": "import A;\nvoid foo() {\n print\n}\n" + } + } +} + +# CHECK: "message"{{.*}}printA{{.*}}(fix available) + +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"file://DIR/Use.cpp"},"context":{"triggerKind":1},"position":{"line":2,"character":6}}} +--- +{"jsonrpc":"2.0","id":2,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/clangd/tool/ClangdMain.cpp =================================================================== --- clang-tools-extra/clangd/tool/ClangdMain.cpp +++ clang-tools-extra/clangd/tool/ClangdMain.cpp @@ -557,6 +557,13 @@ }; #endif +opt ExperimentalModulesSupport{ + "experimental-modules-support", + cat(Features), + desc("Experimental support for standard c++ modules"), + init(false), +}; + /// Supports a test URI scheme with relaxed constraints for lit tests. /// The path in a test URI will be combined with a platform-specific fake /// directory to form an absolute path. For example, test:///a.cpp is resolved @@ -870,6 +877,7 @@ ClangdLSPServer::Options Opts; Opts.UseDirBasedCDB = (CompileArgsFrom == FilesystemCompileArgs); + Opts.ExperimentalModulesSupport = ExperimentalModulesSupport; switch (PCHStorage) { case PCHStorageFlag::Memory: Index: clang-tools-extra/clangd/unittests/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/unittests/CMakeLists.txt +++ clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -72,6 +72,7 @@ LoggerTests.cpp LSPBinderTests.cpp LSPClient.cpp + ModulesManagerTests.cpp ModulesTests.cpp ParsedASTTests.cpp PathMappingTests.cpp Index: clang-tools-extra/clangd/unittests/ModulesManagerTests.cpp =================================================================== --- /dev/null +++ clang-tools-extra/clangd/unittests/ModulesManagerTests.cpp @@ -0,0 +1,350 @@ +//===-- ModulesManagerTests.cpp ---------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "Config.h" +#include "ModulesManager.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/raw_ostream.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace clang; +using namespace clang::clangd; +using namespace llvm; + +namespace { +class ModulesManagerTest : public ::testing::Test { + void SetUp() override { + ASSERT_FALSE(sys::fs::createUniqueDirectory("modules-test", TestDir)); + llvm::errs() << "Created TestDir: " << TestDir << "\n"; + } + + void TearDown() override { + // sys::fs::remove_directories(TestDir); + } + +public: + SmallString<256> TestDir; + + // Add files to the working testing directory and repalce all the + // `__DIR__` to TestDir. + void addFile(StringRef Path, StringRef Contents) { + ASSERT_FALSE(sys::path::is_absolute(Path)); + + SmallString<256> AbsPath(TestDir); + sys::path::append(AbsPath, Path); + + ASSERT_FALSE( + sys::fs::create_directories(llvm::sys::path::parent_path(AbsPath))); + + std::error_code EC; + llvm::raw_fd_ostream OS(AbsPath, EC); + ASSERT_FALSE(EC); + + std::size_t Pos = Contents.find("__DIR__"); + while (Pos != llvm::StringRef::npos) { + OS << Contents.take_front(Pos); + OS << TestDir; + Contents = Contents.drop_front(Pos + sizeof("__DIR__") - 1); + Pos = Contents.find("__DIR__"); + } + + OS << Contents; + } + + // Get the absolute path for file specified by Path under testing working + // directory. + std::string getFullPath(StringRef Path) { + SmallString<128> Result(TestDir); + sys::path::append(Result, Path); + return Result.str().str(); + } +}; + +TEST_F(ModulesManagerTest, ReplaceCommandsTest) { + addFile("build/compile_commands.json", R"cpp( +[ +{ + "directory": "__DIR__", + "command": "clang++ -std=c++20 __DIR__/M.cppm -c -o __DIR__/M.o -fmodule-file=D=__DIR__/D.pcm", + "file": "__DIR__/M.cppm", + "output": "__DIR__/M.o" +} +] + )cpp"); + + addFile("M.cppm", R"cpp( +export module M; +import D; + )cpp"); + + RealThreadsafeFS TFS; + DirectoryBasedGlobalCompilationDatabase::Options Opts(TFS); + DirectoryBasedModulesGlobalCompilationDatabase MCDB(Opts, + /*AsyncThreadsCount*/ 4); + + std::optional Cmd = + MCDB.getCompileCommand(getFullPath("M.cppm")); + EXPECT_TRUE(Cmd); + // Since the graph is not built yet. We don't expect to see the mutated + // command line for modules. + EXPECT_FALSE(any_of(Cmd->CommandLine, [](StringRef Arg) { + return Arg.count("-fprebuilt-module-path"); + })); + EXPECT_TRUE(any_of(Cmd->CommandLine, [](StringRef Arg) { + return Arg.count("-fmodule-file="); + })); + + ModulesManager *MMgr = MCDB.getModulesManager(); + EXPECT_TRUE(MMgr); + MMgr->UpdateNode(getFullPath("M.cppm")); + + MMgr->waitUntilInitialized(); + + Cmd = MCDB.getCompileCommand(getFullPath("M.cppm")); + EXPECT_TRUE(Cmd); + // Since the graph has been built. We expect to see the mutated command line + // for modules. + EXPECT_TRUE(any_of(Cmd->CommandLine, [](StringRef Arg) { + return Arg.count("-fprebuilt-module-path"); + })); + EXPECT_FALSE(any_of(Cmd->CommandLine, [](StringRef Arg) { + return Arg.count("-fmodule-file="); + })); +} + +void AddHelloWorldExample(ModulesManagerTest *Test) { + assert(Test); + + Test->addFile("build/compile_commands.json", R"cpp( +[ +{ + "directory": "__DIR__", + "command": "clang++ -std=c++20 __DIR__/M.cppm -c -o __DIR__/M.o", + "file": "__DIR__/M.cppm", + "output": "__DIR__/M.o" +}, +{ + "directory": "__DIR__", + "command": "clang++ -std=c++20 __DIR__/Impl.cpp -c -o __DIR__/Impl.o", + "file": "__DIR__/Impl.cpp", + "output": "__DIR__/Impl.o" +}, +{ + "directory": "__DIR__", + "command": "clang++ -std=c++20 __DIR__/impl_part.cppm -c -o __DIR__/impl_part.o", + "file": "__DIR__/impl_part.cppm", + "output": "__DIR__/impl_part.o" +}, +{ + "directory": "__DIR__", + "command": "clang++ -std=c++20 __DIR__/interface_part.cppm -c -o __DIR__/interface_part.o", + "file": "__DIR__/interface_part.cppm", + "output": "__DIR__/interface_part.o" +}, +{ + "directory": "__DIR__", + "command": "clang++ -std=c++20 __DIR__/User.cpp -c -o __DIR__/User.o", + "file": "__DIR__/User.cpp", + "output": "__DIR__/User.o" +} +] +)cpp"); + + Test->addFile("M.cppm", R"cpp( +export module M; +export import :interface_part; +import :impl_part; +export void Hello(); + )cpp"); + + Test->addFile("Impl.cpp", R"cpp( +module; +#include "header.mock" +module M; +void Hello() { +} + )cpp"); + + Test->addFile("impl_part.cppm", R"cpp( +module; +#include "header.mock" +module M:impl_part; +import :interface_part; + +void World() { + +} + )cpp"); + + Test->addFile("header.mock", ""); + + Test->addFile("interface_part.cppm", R"cpp( +export module M:interface_part; +export void World(); + )cpp"); + + Test->addFile("User.cpp", R"cpp( +import M; +import third_party_module; +int main() { + Hello(); + World(); + return 0; +} + )cpp"); +} + +TEST_F(ModulesManagerTest, BuildGraphTest) { + AddHelloWorldExample(this); + + RealThreadsafeFS TFS; + DirectoryBasedGlobalCompilationDatabase::Options Opts(TFS); + DirectoryBasedModulesGlobalCompilationDatabase MCDB(Opts, + /*AsyncThreadsCount*/ 4); + + ModulesManager *MMgr = MCDB.getModulesManager(); + EXPECT_TRUE(MMgr); + EXPECT_FALSE(MMgr->HasGraph()); + MMgr->UpdateNode(getFullPath("M.cppm")); + + MMgr->waitUntilInitialized(); + + EXPECT_TRUE(MMgr->HasGraph()); + EXPECT_EQ(MMgr->GraphSize(), 5u); + EXPECT_TRUE(MMgr->IsDirectlyDependent(getFullPath("M.cppm"), + getFullPath("impl_part.cppm"))); + EXPECT_TRUE(MMgr->IsDirectlyDependent(getFullPath("M.cppm"), + getFullPath("interface_part.cppm"))); + EXPECT_TRUE(MMgr->IsDirectlyDependent(getFullPath("impl_part.cppm"), + getFullPath("interface_part.cppm"))); + EXPECT_TRUE(MMgr->IsDirectlyDependent(getFullPath("Impl.cpp"), + getFullPath("M.cppm"))); + EXPECT_TRUE(MMgr->IsDirectlyDependent(getFullPath("User.cpp"), + getFullPath("M.cppm"))); + EXPECT_TRUE(MMgr->HasThirdpartyDependencies(getFullPath("User.cpp"))); +} + +TEST_F(ModulesManagerTest, GenerateModuleInterfaceAndUpdateTest) { + AddHelloWorldExample(this); + + RealThreadsafeFS TFS; + DirectoryBasedGlobalCompilationDatabase::Options Opts(TFS); + Opts.CompileCommandsDir = getFullPath("build"); + DirectoryBasedModulesGlobalCompilationDatabase MCDB(Opts, + /*AsyncThreadsCount*/ 4); + + ModulesManager *MMgr = MCDB.getModulesManager(); + EXPECT_TRUE(MMgr); + + MMgr->UpdateNode(getFullPath("M.cppm")); + + MMgr->waitUntilInitialized(); + + EXPECT_FALSE(MMgr->IsReadyToCompile(getFullPath("User.cpp"))); + + std::condition_variable ReadyCompileCV; + + MMgr->addCallbackAfterReady(getFullPath("User.cpp"), [&ReadyCompileCV](bool) { + ReadyCompileCV.notify_all(); + }); + MMgr->GenerateModuleInterfacesFor(getFullPath("User.cpp")); + + std::mutex Mu; + std::unique_lock Lock(Mu); + ReadyCompileCV.wait(Lock, [MMgr, this]() { + return MMgr->IsReadyToCompile(getFullPath("User.cpp")); + }); + + EXPECT_TRUE(MMgr->IsReadyToCompile(getFullPath("User.cpp"))); + EXPECT_TRUE( + sys::fs::exists(getFullPath("build/.cache/clangd/module_files/M.pcm"))); + EXPECT_TRUE(sys::fs::exists( + getFullPath("build/.cache/clangd/module_files/M-impl_part.pcm"))); + EXPECT_TRUE(sys::fs::exists( + getFullPath("build/.cache/clangd/module_files/M-interface_part.pcm"))); + + MMgr->UpdateNode(getFullPath("User.cpp")); + EXPECT_TRUE(MMgr->IsReadyToCompile(getFullPath("User.cpp"))); + + MMgr->UpdateNode(getFullPath("Impl.cpp")); + EXPECT_TRUE(MMgr->IsReadyToCompile(getFullPath("User.cpp"))); + + MMgr->UpdateNode(getFullPath("M.cppm")); + EXPECT_FALSE(MMgr->IsReadyToCompile(getFullPath("User.cpp"))); +} + +TEST_F(ModulesManagerTest, InvalidBMITest) { + addFile("build/compile_commands.json", R"cpp( +[ +{ + "directory": "__DIR__", + "command": "clang++ -std=c++20 __DIR__/M.cppm -c -o __DIR__/M.o", + "file": "__DIR__/M.cppm", + "output": "__DIR__/M.o" +}, +{ + "directory": "__DIR__", + "command": "clang++ -std=c++20 __DIR__/User.cpp -c -o __DIR__/User.o", + "file": "__DIR__/User.cpp", + "output": "__DIR__/User.o" +} +] + )cpp"); + + addFile("M.cppm", R"cpp( +export module M; +export void Func() { + wlajdliajlwdjawdjlaw // invalid program +} + )cpp"); + + addFile("User.cpp", R"cpp( +import M; +void foo() { + Func(); +} + )cpp"); + + RealThreadsafeFS TFS; + DirectoryBasedGlobalCompilationDatabase::Options Opts(TFS); + DirectoryBasedModulesGlobalCompilationDatabase MCDB(Opts, + /*AsyncThreadsCount*/ 4); + ModulesManager *MMgr = MCDB.getModulesManager(); + EXPECT_TRUE(MMgr); + MMgr->UpdateNode(getFullPath("User.cpp")); + MMgr->waitUntilInitialized(); + + EXPECT_FALSE(MMgr->IsReadyToCompile(getFullPath("User.cpp"))); + EXPECT_FALSE(MMgr->HasInvalidDependencies(getFullPath("User.cpp"))); + + std::condition_variable ReadyCompileCV; + bool Failed = false; + + MMgr->addCallbackAfterReady(getFullPath("User.cpp"), + [&ReadyCompileCV, &Failed](bool Ready) { + Failed = !Ready; + ReadyCompileCV.notify_all(); + }); + MMgr->GenerateModuleInterfacesFor(getFullPath("User.cpp")); + + std::mutex Mu; + std::unique_lock Lock(Mu); + ReadyCompileCV.wait(Lock, [MMgr, this]() { + return MMgr->HasInvalidDependencies(getFullPath("User.cpp")); + }); + + EXPECT_TRUE(MMgr->HasInvalidDependencies(getFullPath("User.cpp"))); + // Make sure that the failure callback are called. + EXPECT_TRUE(Failed); +} + +} // anonymous namespace Index: clang-tools-extra/docs/ReleaseNotes.rst =================================================================== --- clang-tools-extra/docs/ReleaseNotes.rst +++ clang-tools-extra/docs/ReleaseNotes.rst @@ -48,6 +48,9 @@ Improvements to clangd ---------------------- +- Implemented the experimental support for C++20 modules. This can be enabled by + `-experimental-modules-support` option. + Inlay hints ^^^^^^^^^^^