================ @@ -0,0 +1,1507 @@ +//===--- ModulesDriver.cpp - Driver managed module builds -----------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +/// +/// \file +/// This file defines functionality to support driver managed builds for +/// compilations which use Clang modules or standard C++20 named modules. +/// +//===----------------------------------------------------------------------===// + +#include "clang/Driver/ModulesDriver.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/LLVM.h" +#include "clang/DependencyScanning/DependencyScanningUtils.h" +#include "clang/Driver/Compilation.h" +#include "clang/Driver/Driver.h" +#include "clang/Driver/Job.h" +#include "clang/Driver/Tool.h" +#include "clang/Driver/ToolChain.h" +#include "clang/Frontend/StandaloneDiagnostic.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/DepthFirstIterator.h" +#include "llvm/ADT/DirectedGraph.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/ADT/iterator_range.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/GraphWriter.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/PrettyStackTrace.h" +#include "llvm/Support/ThreadPool.h" +#include <algorithm> +#include <utility> + +namespace deps = clang::dependencies; + +using namespace llvm::opt; +using namespace clang; +using namespace driver; +using namespace modules; + +namespace clang::driver::modules { +static bool fromJSON(const llvm::json::Value &Params, + StdModuleManifest::Module::LocalArguments &LocalArgs, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O.mapOptional("system-include-directories", + LocalArgs.SystemIncludeDirs); +} + +static bool fromJSON(const llvm::json::Value &Params, + StdModuleManifest::Module &ModuleEntry, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O.map("is-std-library", ModuleEntry.IsStdlib) && + O.map("logical-name", ModuleEntry.LogicalName) && + O.map("source-path", ModuleEntry.SourcePath) && + O.mapOptional("local-arguments", ModuleEntry.LocalArgs); +} + +static bool fromJSON(const llvm::json::Value &Params, + StdModuleManifest &Manifest, llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O.map("modules", Manifest.Modules); +} +} // namespace clang::driver::modules + +/// Parses the Standard library module manifest from \p Buffer. +static Expected<StdModuleManifest> parseManifest(StringRef Buffer) { + auto ParsedOrErr = llvm::json::parse(Buffer); + if (!ParsedOrErr) + return ParsedOrErr.takeError(); + + StdModuleManifest Manifest; + llvm::json::Path::Root Root; + if (!fromJSON(*ParsedOrErr, Manifest, Root)) + return Root.getError(); + + return Manifest; +} + +/// Converts each file path in manifest from relative to absolute. +/// +/// Each file path in the manifest is expected to be relative the manifest's +/// location \p ManifestPath itself. +static void makeManifestPathsAbsolute( + MutableArrayRef<StdModuleManifest::Module> ManifestEntries, + StringRef ManifestPath) { + StringRef ManifestDir = llvm::sys::path::parent_path(ManifestPath); + SmallString<256> TempPath; + + auto PrependManifestDir = [&](StringRef Path) { + TempPath = ManifestDir; + llvm::sys::path::append(TempPath, Path); + return std::string(TempPath); + }; + + for (auto &Entry : ManifestEntries) { + Entry.SourcePath = PrependManifestDir(Entry.SourcePath); + if (!Entry.LocalArgs) + continue; + + for (auto &IncludeDir : Entry.LocalArgs->SystemIncludeDirs) + IncludeDir = PrependManifestDir(IncludeDir); + } +} + +Expected<StdModuleManifest> +driver::modules::readStdModuleManifest(StringRef ManifestPath, + llvm::vfs::FileSystem &VFS) { + auto MemBufOrErr = VFS.getBufferForFile(ManifestPath); + if (!MemBufOrErr) + return llvm::createFileError(ManifestPath, MemBufOrErr.getError()); + + auto ManifestOrErr = parseManifest((*MemBufOrErr)->getBuffer()); + if (!ManifestOrErr) + return ManifestOrErr.takeError(); + auto Manifest = std::move(*ManifestOrErr); + + makeManifestPathsAbsolute(Manifest.Modules, ManifestPath); + return Manifest; +} + +void driver::modules::buildStdModuleManifestInputs( + ArrayRef<StdModuleManifest::Module> ManifestEntries, Compilation &C, + InputList &Inputs) { + DerivedArgList &Args = C.getArgs(); + const OptTable &Opts = C.getDriver().getOpts(); + for (const auto &Entry : ManifestEntries) { + auto *InputArg = + makeInputArg(Args, Opts, Args.MakeArgString(Entry.SourcePath)); + Inputs.emplace_back(types::TY_CXXModule, InputArg); + } +} + +/// Computes the -fmodule-cache-path for this compilation. +static std::optional<std::string> +getModuleCachePath(llvm::opt::DerivedArgList &Args) { + if (const Arg *A = Args.getLastArg(options::OPT_fmodules_cache_path)) + return A->getValue(); + + if (SmallString<128> Path; Driver::getDefaultModuleCachePath(Path)) + return std::string(Path); + + return std::nullopt; +} + +using ManifestEntryLookup = + llvm::DenseMap<StringRef, const StdModuleManifest::Module *>; + +/// Builds a mapping from a module's source path to its entry in the manifest. +static ManifestEntryLookup +buildManifestLookupMap(ArrayRef<StdModuleManifest::Module> ManifestEntries) { + ManifestEntryLookup ManifestEntryBySource; + for (auto &Entry : ManifestEntries) { + const bool Inserted = + ManifestEntryBySource.try_emplace(Entry.SourcePath, &Entry).second; + assert(Inserted && + "Manifest defines multiple modules with the same source path."); + } + return ManifestEntryBySource; +} + +/// Returns the manifest entry corresponding to \p Job, or \c nullptr if none +/// exists. +static const StdModuleManifest::Module * +getManifestEntryForCommand(const Command &Job, + const ManifestEntryLookup &ManifestLookup) { + for (const auto &II : Job.getInputInfos()) { + if (const auto It = ManifestLookup.find(II.getFilename()); + It != ManifestLookup.end()) + return It->second; + } + return nullptr; +} + +/// Adds all \p SystemIncludeDirs to \p Job's arguments. +static void +addSystemIncludeDirsFromManifest(Compilation &C, Command &Job, + ArrayRef<std::string> SystemIncludeDirs) { + const ToolChain &TC = Job.getCreator().getToolChain(); + const DerivedArgList &TCArgs = + C.getArgsForToolChain(&TC, Job.getSource().getOffloadingArch(), + Job.getSource().getOffloadingDeviceKind()); + + ArgStringList NewArgs = Job.getArguments(); + for (const auto &IncludeDir : SystemIncludeDirs) + TC.addSystemInclude(TCArgs, NewArgs, IncludeDir); + Job.replaceArguments(NewArgs); +} + +static bool isCC1Job(const Command &Job) { + return StringRef(Job.getCreator().getName()) == "clang"; +} + +/// For each job that generates a Standard library module, applies any +/// local arguments specified in the corresponding module manifest entry. +static void +applyLocalArgsFromManifest(Compilation &C, + const ManifestEntryLookup &ManifestLookup, + MutableArrayRef<std::unique_ptr<Command>> Jobs) { + for (auto &Job : Jobs) { + if (!isCC1Job(*Job)) + continue; + + const auto *Entry = getManifestEntryForCommand(*Job, ManifestLookup); + if (!Entry || !Entry->LocalArgs) + continue; + + const auto &IncludeDirs = Entry->LocalArgs->SystemIncludeDirs; + addSystemIncludeDirsFromManifest(C, *Job, IncludeDirs); + } +} + +/// Returns true if a dependency scan can be performed using \p Job. +static bool isDependencyScannableJob(const Command &Job) { + if (!isCC1Job(Job)) + return false; + const auto &InputInfos = Job.getInputInfos(); + return !InputInfos.empty() && types::isSrcFile(InputInfos.front().getType()); +} + +namespace { +/// Pool of reusable dependency scanning workers and their contexts with +/// RAII-based acquire/release. +class ScanningWorkerPool { +public: + ScanningWorkerPool(size_t NumWorkers, + deps::DependencyScanningService &ScanningService) { + for (size_t I = 0; I < NumWorkers; ++I) + Slots.emplace_back(ScanningService); + + AvailableSlots.resize(NumWorkers); + std::iota(AvailableSlots.begin(), AvailableSlots.end(), 0); + } + + /// Acquires a unique pointer to a dependency scanning worker and its + /// context. + /// + /// The worker bundle automatically released back to the pool when the + /// pointer is destroyed. The pool has to outlive the leased worker bundle. + [[nodiscard]] auto scopedAcquire() { + std::unique_lock<std::mutex> UL(Lock); + CV.wait(UL, [&] { return !AvailableSlots.empty(); }); + const size_t Index = AvailableSlots.pop_back_val(); + auto ReleaseHandle = [this, Index](WorkerBundle *) { release(Index); }; + return std::unique_ptr<WorkerBundle, decltype(ReleaseHandle)>( + &Slots[Index], ReleaseHandle); + } + +private: + /// Releases the worker bundle at \c Index back into the pool. + void release(size_t Index) { + { + std::scoped_lock<std::mutex> SL(Lock); + AvailableSlots.push_back(Index); + } + CV.notify_one(); + } + + /// A scanning worker with its associated context. + struct WorkerBundle { + WorkerBundle(deps::DependencyScanningService &ScanningService) + : Worker(std::make_unique<deps::DependencyScanningWorker>( + ScanningService)) {} + + std::unique_ptr<deps::DependencyScanningWorker> Worker; + llvm::DenseSet<deps::ModuleID> SeenModules; + }; + + std::mutex Lock; + std::condition_variable CV; + SmallVector<size_t> AvailableSlots; + SmallVector<WorkerBundle, 0> Slots; +}; +} // anonymous namespace + +// Creates a ThreadPool and a corresponding ScanningWorkerPool optimized for +// the configuration of dependency scan inputs. +static std::pair<std::unique_ptr<llvm::ThreadPoolInterface>, + std::unique_ptr<ScanningWorkerPool>> +createOptimalThreadAndWorkerPool( + size_t NumScanInputs, bool HasStdlibModuleInputs, + deps::DependencyScanningService &ScanningService) { + // TODO: Benchmark: Determine the optimal number of worker threads for a + // given number of inputs. How many inputs are required for multi-threading + // to be beneficial? How many inputs should each thread scan at least? +#if LLVM_ENABLE_THREADS + std::unique_ptr<llvm::ThreadPoolInterface> ThreadPool; + size_t WorkerCount; + + if (NumScanInputs == 1 || (HasStdlibModuleInputs && NumScanInputs <= 2)) { + auto S = llvm::optimal_concurrency(1); + ThreadPool = std::make_unique<llvm::SingleThreadExecutor>(std::move(S)); + WorkerCount = 1; + } else { + auto ThreadPoolStrategy = llvm::optimal_concurrency( + NumScanInputs - static_cast<size_t>(HasStdlibModuleInputs)); + ThreadPool = std::make_unique<llvm::DefaultThreadPool>( + std::move(ThreadPoolStrategy)); + const size_t MaxConcurrency = ThreadPool->getMaxConcurrency(); + const size_t MaxConcurrentlyScannedInputs = + NumScanInputs - + (HasStdlibModuleInputs && NumScanInputs < MaxConcurrency ? 1 : 0); + WorkerCount = std::min(MaxConcurrency, MaxConcurrentlyScannedInputs); + } +#else + auto ThreadPool = std::make_unique<llvm::SingleThreadExecutor>(); + size_t WorkerCount = 1; +#endif + + return {std::move(ThreadPool), + std::make_unique<ScanningWorkerPool>(WorkerCount, ScanningService)}; +} + +static StringRef getTriple(const Command &Job) { + return Job.getCreator().getToolChain().getTriple().getTriple(); +} + +using ModuleNameAndTriple = std::pair<StringRef, StringRef>; + +namespace { +/// Thread-safe registry of Standard library scan inputs. +struct StdlibModuleScanScheduler { + StdlibModuleScanScheduler(const llvm::DenseMap<ModuleNameAndTriple, size_t> + &StdlibModuleScanIndexByID) + : StdlibModuleScanIndexByID(StdlibModuleScanIndexByID) { + ScheduledScanInputs.reserve(StdlibModuleScanIndexByID.size()); + } + + /// Returns the indices of scan inputs corresponding to newly imported + /// Standard library modules. + /// + /// Thread-safe. + SmallVector<size_t, 2> getNewScanInputs(ArrayRef<std::string> NamedModuleDeps, + StringRef Triple) { + SmallVector<size_t, 2> NewScanInputs; + std::scoped_lock<std::mutex> Guard(Lock); + for (const auto &ModuleName : NamedModuleDeps) { + const auto It = StdlibModuleScanIndexByID.find({ModuleName, Triple}); + if (It == StdlibModuleScanIndexByID.end()) + continue; + const size_t ScanIndex = It->second; + const bool AlreadyScheduled = + !ScheduledScanInputs.insert(ScanIndex).second; + if (AlreadyScheduled) + continue; + NewScanInputs.push_back(ScanIndex); + } + return NewScanInputs; + } + +private: + const llvm::DenseMap<ModuleNameAndTriple, size_t> &StdlibModuleScanIndexByID; + llvm::SmallDenseSet<size_t> ScheduledScanInputs; + std::mutex Lock; +}; + +/// Collects diagnostics in a form that can be retained until after their +/// associated SourceManager is destroyed. +class StandaloneDiagCollector : public DiagnosticConsumer { +public: + void BeginSourceFile(const LangOptions &LangOpts, + const Preprocessor *PP = nullptr) override { + this->LangOpts = &LangOpts; + } + + void HandleDiagnostic(DiagnosticsEngine::Level Level, + const Diagnostic &Info) override { + StoredDiagnostic StoredDiag(Level, Info); + StandaloneDiags.emplace_back(*LangOpts, StoredDiag); + DiagnosticConsumer::HandleDiagnostic(Level, Info); + } + + SmallVector<StandaloneDiagnostic, 0> takeDiagnostics() { + return std::move(StandaloneDiags); + } + +private: + const LangOptions *LangOpts = nullptr; + SmallVector<StandaloneDiagnostic, 0> StandaloneDiags; +}; + +/// RAII utility to report collected StandaloneDiagnostic through a +/// DiagnosticsEngine. +/// +/// The driver's DiagnosticsEngine usually does not have a SourceManager at +/// this point of building the compilation, in which case the +/// StandaloneDiagReporter supplies its own. +class StandaloneDiagReporter { +public: + explicit StandaloneDiagReporter(DiagnosticsEngine &Diags) : Diags(Diags) { + if (!Diags.hasSourceManager()) { + FileSystemOptions Opts; + Opts.WorkingDir = "."; + OwnedFileMgr = llvm::makeIntrusiveRefCnt<FileManager>(std::move(Opts)); + OwnedSrcMgr = + llvm::makeIntrusiveRefCnt<SourceManager>(Diags, *OwnedFileMgr); + } + } + + /// Emits all diagnostics in \c StandaloneDiags using the associated + /// DiagnosticsEngine. + void Report(ArrayRef<StandaloneDiagnostic> StandaloneDiags) const { + llvm::StringMap<SourceLocation> SrcLocCache; + Diags.getClient()->BeginSourceFile(LangOptions(), nullptr); + for (auto &StandaloneDiag : StandaloneDiags) { + const auto StoredDiag = translateStandaloneDiag( + getFileManager(), getSourceManager(), StandaloneDiag, SrcLocCache); + Diags.Report(StoredDiag); + } + Diags.getClient()->EndSourceFile(); + } + +private: + DiagnosticsEngine &Diags; + IntrusiveRefCntPtr<FileManager> OwnedFileMgr; + IntrusiveRefCntPtr<SourceManager> OwnedSrcMgr; + + FileManager &getFileManager() const { + if (OwnedFileMgr) + return *OwnedFileMgr; + return Diags.getSourceManager().getFileManager(); + } + + SourceManager &getSourceManager() const { + if (OwnedSrcMgr) + return *OwnedSrcMgr; + return Diags.getSourceManager(); + } +}; +} // anonymous namespace + +/// Report the diagnostics collected during each dependency scan. +static void reportAllScanDiagnostics( + SmallVectorImpl<SmallVector<StandaloneDiagnostic, 0>> &&AllScanDiags, + DiagnosticsEngine &Diags) { + StandaloneDiagReporter Reporter(Diags); + for (auto &SingleScanDiags : AllScanDiags) + Reporter.Report(SingleScanDiags); +} + +/// Construct a path for the explicitly built PCM. +static std::string constructPCMPath(const deps::ModuleID &ID, + StringRef OutputDir) { + assert(!ID.ModuleName.empty() && !ID.ContextHash.empty() && + "Invalid ModuleID!"); + SmallString<256> ExplicitPCMPath(OutputDir); + llvm::sys::path::append(ExplicitPCMPath, ID.ContextHash, + ID.ModuleName + "-" + ID.ContextHash + ".pcm"); + return std::string(ExplicitPCMPath); +} + +namespace { +/// A simple dependency action controller that only provides module lookup for +/// Clang modules. +class ModuleLookupController : public deps::DependencyActionController { +public: + ModuleLookupController(StringRef OutputDir) : OutputDir(OutputDir) {} + + std::string lookupModuleOutput(const deps::ModuleDeps &MD, + deps::ModuleOutputKind Kind) override { + if (Kind == deps::ModuleOutputKind::ModuleFile) + return constructPCMPath(MD.ID, OutputDir); + + // Driver command lines that trigger lookups for unsupported + // ModuleOutputKinds are not supported by the modules driver. Those + // command lines should probably be adjusted or rejected in + // Driver::handleArguments or Driver::HandleImmediateArgs. + llvm::reportFatalInternalError( + "call to lookupModuleOutput with unexpected ModuleOutputKind"); + } + +private: + StringRef OutputDir; +}; + +/// The full dependencies for a specific command-line input. +struct InputDependencies { + /// The name of the C++20 module provided by this translation unit. + std::string ModuleName; + + /// A list of modules this translation unit directly depends on, not including + /// transitive dependencies. + /// + /// This may include modules with a different context hash when it can be + /// determined that the differences are benign for this compilation. + std::vector<deps::ModuleID> ClangModuleDeps; + + /// A list of the C++20 named modules this translation unit depends on. + /// + /// All C++20 named module dependencies are expected to target the same triple + /// as this translation unit. + std::vector<std::string> NamedModuleDeps; + + /// A collection of absolute paths to files that this translation unit + /// directly depends on, not including transitive dependencies. + std::vector<std::string> FileDeps; + + /// The compiler invocation with modifications to properly import all Clang + /// module dependencies. Does not include argv[0]. + std::vector<std::string> BuildArgs; +}; +} // anonymous namespace + +static InputDependencies makeInputDeps(deps::TranslationUnitDeps &&TUDeps) { + InputDependencies InputDeps; + InputDeps.ModuleName = std::move(TUDeps.ID.ModuleName); + InputDeps.NamedModuleDeps = std::move(TUDeps.NamedModuleDeps); + InputDeps.ClangModuleDeps = std::move(TUDeps.ClangModuleDeps); + InputDeps.FileDeps = std::move(TUDeps.FileDeps); + assert(TUDeps.Commands.size() == 1 && "Expected exactly one command"); + InputDeps.BuildArgs = std::move(TUDeps.Commands.front().Arguments); + return InputDeps; +} + +/// Constructs the full command line, including the executable, for \p Job. +static SmallVector<std::string, 0> buildCommandLine(const Command &Job) { + const auto &JobArgs = Job.getArguments(); + SmallVector<std::string, 0> CommandLine; + CommandLine.reserve(JobArgs.size() + 1); + CommandLine.emplace_back(Job.getExecutable()); + for (const char *Arg : JobArgs) + CommandLine.emplace_back(Arg); + return CommandLine; +} + +/// Performs a dependency scan for a single job. +/// +/// \returns a pair containing TranslationUnitDeps on success, or std::nullopt +/// on failure, along with any diagnostics produced. +static std::pair<std::optional<deps::TranslationUnitDeps>, + SmallVector<StandaloneDiagnostic, 0>> +scanDependenciesForJob(const Command &Job, ScanningWorkerPool &WorkerPool, + ModuleLookupController &LookupController) { + StandaloneDiagCollector DiagConsumer; + std::optional<deps::TranslationUnitDeps> MaybeTUDeps; + + { + const auto CC1CommandLine = buildCommandLine(Job); + auto WorkerBundleHandle = WorkerPool.scopedAcquire(); + deps::FullDependencyConsumer DepConsumer(WorkerBundleHandle->SeenModules); + + if (WorkerBundleHandle->Worker->computeDependencies( + /*WorkingDirectory*/ ".", CC1CommandLine, DepConsumer, + LookupController, DiagConsumer)) + MaybeTUDeps = DepConsumer.takeTranslationUnitDeps(); + } + + return {std::move(MaybeTUDeps), DiagConsumer.takeDiagnostics()}; +} + +namespace { +struct DependencyScanResult { + /// Indices of jobs that were successfully scanned. + SmallVector<size_t> ScannedJobIndices; + + /// Input dependencies for scanned jobs. Parallel to \c ScannedJobIndices. + SmallVector<InputDependencies, 0> InputDepsForScannedJobs; + + /// Module dependency graphs for scanned jobs. Parallel to \c + /// ScannedJobIndices. + SmallVector<deps::ModuleDepsGraph, 0> ModuleDepGraphsForScannedJobs; + + /// Indices of Standard library module jobs not discovered as dependencies. + SmallVector<size_t> UnusedStdlibModuleJobIndices; + + /// Indices of jobs that could not be scanned (e.g. image jobs, ...). + SmallVector<size_t> NonScannableJobIndices; +}; +} // anonymous namespace + +/// Scans the compilations job list \p Jobs for module dependencies. +/// +/// Standard library module jobs are scanned on demand if imported by any +/// user-provided input. +/// +/// \returns DependencyScanResult on success, or std::nullopt on failure, with +/// diagnostics reported via \p Diags in both cases. +static std::optional<DependencyScanResult> scanDependencies( + ArrayRef<std::unique_ptr<Command>> Jobs, + llvm::DenseMap<StringRef, const StdModuleManifest::Module *> ManifestLookup, + StringRef ModuleCachePath, IntrusiveRefCntPtr<llvm::vfs::FileSystem> BaseFS, + DiagnosticsEngine &Diags) { + llvm::PrettyStackTraceString CrashInfo("Performing module dependency scan."); + + // Classify the jobs based on scan eligibility. + SmallVector<size_t> ScannableJobIndices; + SmallVector<size_t> NonScannableJobIndices; + for (auto &&[Index, Job] : llvm::enumerate(Jobs)) { + if (isDependencyScannableJob(*Job)) + ScannableJobIndices.push_back(Index); + else + NonScannableJobIndices.push_back(Index); + } + + // Classify scannable jobs by origin. User-provided inputs will be scanned + // immediately, while Standard library modules are indexed for on-demand + // scanning when discovered as dependencies. + SmallVector<size_t> UserInputScanIndices; + llvm::DenseMap<ModuleNameAndTriple, size_t> StdlibModuleScanIndexByID; + for (auto &&[ScanIndex, JobIndex] : llvm::enumerate(ScannableJobIndices)) { + const Command &ScanJob = *Jobs[JobIndex]; + if (const auto *Entry = + getManifestEntryForCommand(ScanJob, ManifestLookup)) { + ModuleNameAndTriple ID{Entry->LogicalName, getTriple(ScanJob)}; + const bool Inserted = + StdlibModuleScanIndexByID.try_emplace(ID, ScanIndex).second; + assert(Inserted && + "Multiple jobs build the same module for the same triple."); + } else { + UserInputScanIndices.push_back(ScanIndex); + } + } + + // Initialize the scan context. + const size_t NumScanInputs = ScannableJobIndices.size(); + const bool HasStdlibModuleInputs = !StdlibModuleScanIndexByID.empty(); + + deps::DependencyScanningServiceOptions Opts; + Opts.MakeVFS = [&] { return BaseFS; }; ---------------- naveen-seth wrote:
Addressed in [65cf582](https://github.com/llvm/llvm-project/pull/152770/commits/65cf58254ece12180db4b5b4fd4dd85f8678c34d). https://github.com/llvm/llvm-project/pull/152770 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
