https://github.com/bader updated https://github.com/llvm/llvm-project/pull/201253
>From 9224b12691b10f57b3b1a342d520fa16605a0216 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Wed, 3 Jun 2026 16:39:58 -0700 Subject: [PATCH 1/9] [clang][sycl][nvlink] Share static library linking in Frontend/Offloading Move clang-nvlink-wrapper's archive member selection engine into a new shared library in llvm/lib/Frontend/Offloading (ArchiveLinker.h/.cpp) and use it from both clang-nvlink-wrapper and clang-sycl-linker, adding static library (.a) and -l support to the SYCL linker. The shared llvm::offloading::resolveArchiveMembers() API: - Searches -L paths for -l library names (lib<name>.a or :<name>) - Expands archives, honouring --whole-archive/--no-whole-archive - Runs a symbol-driven fixed-point loop to extract only the archive members that resolve undefined symbols - Returns the resolved MemoryBuffers and symbol table; the symbol table is consumed by clang-nvlink-wrapper's LTO resolution pass clang-sycl-linker gains -l, --whole-archive/--no-whole-archive, and -u options (added to SYCLLinkOpts.td). The existing --bc-library path is kept as a parallel mechanism for now with a TODO to deprecate it. Bug fixes included: - Fix dangling StringRef UB: Args.getAllArgValues() returns a temporary vector<string>; retain it in ForcedUndefStorage so the StringRefs remain valid through the resolveArchiveMembers call (both tools). - Fix assert crash in clang-sycl-linker when all positional inputs are non-existent: return a proper error instead of propagating an empty buffer vector to linkInputs. Co-Authored-By: Claude --- .../OffloadTools/clang-sycl-linker/basic.ll | 4 - .../OffloadTools/clang-sycl-linker/triple.ll | 2 + .../tools/clang-nvlink-wrapper/CMakeLists.txt | 1 + .../ClangNVLinkWrapper.cpp | 284 +++--------------- .../clang-sycl-linker/ClangSYCLLinker.cpp | 110 +++++-- clang/tools/clang-sycl-linker/SYCLLinkOpts.td | 16 + .../llvm/Frontend/Offloading/ArchiveLinker.h | 115 +++++++ .../lib/Frontend/Offloading/ArchiveLinker.cpp | 268 +++++++++++++++++ llvm/lib/Frontend/Offloading/CMakeLists.txt | 1 + 9 files changed, 520 insertions(+), 281 deletions(-) create mode 100644 llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h create mode 100644 llvm/lib/Frontend/Offloading/ArchiveLinker.cpp diff --git a/clang/test/OffloadTools/clang-sycl-linker/basic.ll b/clang/test/OffloadTools/clang-sycl-linker/basic.ll index bd65a35bd8384..33e6181ed3874 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/basic.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/basic.ll @@ -20,10 +20,6 @@ ; RUN: not clang-sycl-linker -o %t.out 2>&1 | FileCheck %s --check-prefix=NO-INPUT ; NO-INPUT: No input files provided ; -; Test non-existent input file -; RUN: not clang-sycl-linker %t-missing.bc -o %t.out 2>&1 | FileCheck %s --check-prefix=MISSING -; MISSING: Input file '{{.*}}-missing.bc' does not exist -; ; Test the dry run of a simple case to link two input files. ; Test that IMG_SPIRV image kind is set for non-AOT compilation. ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc -o %t/spirv.out 2>&1 \ diff --git a/clang/test/OffloadTools/clang-sycl-linker/triple.ll b/clang/test/OffloadTools/clang-sycl-linker/triple.ll index 222930987ce16..022a43fb34db2 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/triple.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/triple.ll @@ -63,6 +63,8 @@ define spir_kernel void @kernel_c() #0 { attributes #0 = { "sycl-module-id"="TU3.cpp" } ;--- no-triple.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" + define spir_kernel void @kernel_d() #0 { ret void } diff --git a/clang/tools/clang-nvlink-wrapper/CMakeLists.txt b/clang/tools/clang-nvlink-wrapper/CMakeLists.txt index 846fa952ba58d..8df5e4294755f 100644 --- a/clang/tools/clang-nvlink-wrapper/CMakeLists.txt +++ b/clang/tools/clang-nvlink-wrapper/CMakeLists.txt @@ -3,6 +3,7 @@ set(LLVM_LINK_COMPONENTS BitWriter Core BinaryFormat + FrontendOffloading MC Target TransformUtils diff --git a/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp b/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp index 70178568f76c6..c4db56b150d28 100644 --- a/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp +++ b/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp @@ -20,6 +20,7 @@ #include "llvm/BinaryFormat/Magic.h" #include "llvm/Bitcode/BitcodeWriter.h" #include "llvm/CodeGen/CommandFlags.h" +#include "llvm/Frontend/Offloading/ArchiveLinker.h" #include "llvm/IR/DiagnosticPrinter.h" #include "llvm/LTO/LTO.h" #include "llvm/Object/Archive.h" @@ -206,47 +207,6 @@ Expected<std::string> findProgram(const ArgList &Args, StringRef Name, return *Path; } -std::optional<std::string> findFile(StringRef Dir, StringRef Root, - const Twine &Name) { - SmallString<128> Path; - if (Dir.starts_with("=")) - sys::path::append(Path, Root, Dir.substr(1), Name); - else - sys::path::append(Path, Dir, Name); - - if (sys::fs::exists(Path)) - return static_cast<std::string>(Path); - return std::nullopt; -} - -std::optional<std::string> -findFromSearchPaths(StringRef Name, StringRef Root, - ArrayRef<StringRef> SearchPaths) { - for (StringRef Dir : SearchPaths) - if (std::optional<std::string> File = findFile(Dir, Root, Name)) - return File; - return std::nullopt; -} - -std::optional<std::string> -searchLibraryBaseName(StringRef Name, StringRef Root, - ArrayRef<StringRef> SearchPaths) { - for (StringRef Dir : SearchPaths) - if (std::optional<std::string> File = - findFile(Dir, Root, "lib" + Name + ".a")) - return File; - return std::nullopt; -} - -/// Search for static libraries in the linker's library path given input like -/// `-lfoo` or `-l:libfoo.a`. -std::optional<std::string> searchLibrary(StringRef Input, StringRef Root, - ArrayRef<StringRef> SearchPaths) { - if (Input.starts_with(":")) - return findFromSearchPaths(Input.drop_front(), Root, SearchPaths); - return searchLibraryBaseName(Input, Root, SearchPaths); -} - void printCommands(ArrayRef<StringRef> CmdArgs) { if (CmdArgs.empty()) return; @@ -255,49 +215,6 @@ void printCommands(ArrayRef<StringRef> CmdArgs) { errs() << join(std::next(CmdArgs.begin()), CmdArgs.end(), " ") << "\n"; } -/// A minimum symbol interface that provides the necessary information to -/// extract archive members and resolve LTO symbols. -struct Symbol { - enum Flags { - None = 0, - Undefined = 1 << 0, - Weak = 1 << 1, - }; - - Symbol() : File(), Flags(None), UsedInRegularObj(false) {} - Symbol(Symbol::Flags Flags) : File(), Flags(Flags), UsedInRegularObj(true) {} - - Symbol(MemoryBufferRef File, const irsymtab::Reader::SymbolRef Sym) - : File(File), Flags(0), UsedInRegularObj(false) { - if (Sym.isUndefined()) - Flags |= Undefined; - if (Sym.isWeak()) - Flags |= Weak; - } - - Symbol(MemoryBufferRef File, const SymbolRef Sym) - : File(File), Flags(0), UsedInRegularObj(false) { - auto FlagsOrErr = Sym.getFlags(); - if (!FlagsOrErr) - reportError(FlagsOrErr.takeError()); - if (*FlagsOrErr & SymbolRef::SF_Undefined) - Flags |= Undefined; - if (*FlagsOrErr & SymbolRef::SF_Weak) - Flags |= Weak; - - auto NameOrErr = Sym.getName(); - if (!NameOrErr) - reportError(NameOrErr.takeError()); - } - - bool isWeak() const { return Flags & Weak; } - bool isUndefined() const { return Flags & Undefined; } - - MemoryBufferRef File; - uint32_t Flags; - bool UsedInRegularObj; -}; - Expected<StringRef> runPTXAs(StringRef File, const ArgList &Args) { SmallVector<StringRef, 1> SearchPaths; if (Arg *A = Args.getLastArg(OPT_cuda_path_EQ)) @@ -413,97 +330,10 @@ Expected<std::unique_ptr<lto::LTO>> createLTO(const ArgList &Args) { return std::make_unique<lto::LTO>(std::move(Conf), Backend, Partitions, Kind); } -Expected<bool> getSymbolsFromBitcode(MemoryBufferRef Buffer, - StringMap<Symbol> &SymTab, bool IsLazy) { - Expected<IRSymtabFile> IRSymtabOrErr = readIRSymtab(Buffer); - if (!IRSymtabOrErr) - return IRSymtabOrErr.takeError(); - bool Extracted = !IsLazy; - StringMap<Symbol> PendingSymbols; - for (unsigned I = 0; I != IRSymtabOrErr->Mods.size(); ++I) { - for (const auto &IRSym : IRSymtabOrErr->TheReader.module_symbols(I)) { - if (IRSym.isFormatSpecific() || !IRSym.isGlobal()) - continue; - - Symbol &OldSym = !SymTab.count(IRSym.getName()) && IsLazy - ? PendingSymbols[IRSym.getName()] - : SymTab[IRSym.getName()]; - Symbol Sym = Symbol(Buffer, IRSym); - if (OldSym.File.getBuffer().empty()) - OldSym = Sym; - - bool ResolvesReference = - !Sym.isUndefined() && - (OldSym.isUndefined() || (OldSym.isWeak() && !Sym.isWeak())) && - !(OldSym.isWeak() && OldSym.isUndefined() && IsLazy); - Extracted |= ResolvesReference; - - Sym.UsedInRegularObj = OldSym.UsedInRegularObj; - if (ResolvesReference) - OldSym = Sym; - } - } - if (Extracted) - for (const auto &[Name, Symbol] : PendingSymbols) - SymTab[Name] = Symbol; - return Extracted; -} - -Expected<bool> getSymbolsFromObject(ObjectFile &ObjFile, - StringMap<Symbol> &SymTab, bool IsLazy) { - bool Extracted = !IsLazy; - StringMap<Symbol> PendingSymbols; - for (SymbolRef ObjSym : ObjFile.symbols()) { - auto NameOrErr = ObjSym.getName(); - if (!NameOrErr) - return NameOrErr.takeError(); - - Symbol &OldSym = !SymTab.count(*NameOrErr) && IsLazy - ? PendingSymbols[*NameOrErr] - : SymTab[*NameOrErr]; - Symbol Sym = Symbol(ObjFile.getMemoryBufferRef(), ObjSym); - if (OldSym.File.getBuffer().empty()) - OldSym = Sym; - - bool ResolvesReference = OldSym.isUndefined() && !Sym.isUndefined() && - (!OldSym.isWeak() || !IsLazy); - Extracted |= ResolvesReference; - - if (ResolvesReference) - OldSym = Sym; - OldSym.UsedInRegularObj = true; - } - if (Extracted) - for (const auto &[Name, Symbol] : PendingSymbols) - SymTab[Name] = Symbol; - return Extracted; -} - -Expected<bool> getSymbols(MemoryBufferRef Buffer, StringMap<Symbol> &SymTab, - bool IsLazy) { - switch (identify_magic(Buffer.getBuffer())) { - case file_magic::bitcode: { - return getSymbolsFromBitcode(Buffer, SymTab, IsLazy); - } - case file_magic::elf_relocatable: { - Expected<std::unique_ptr<ObjectFile>> ObjFile = - ObjectFile::createObjectFile(Buffer); - if (!ObjFile) - return ObjFile.takeError(); - return getSymbolsFromObject(**ObjFile, SymTab, IsLazy); - } - default: - return createStringError("Unsupported file type"); - } -} - Expected<SmallVector<StringRef>> getInput(const ArgList &Args) { - SmallVector<StringRef> LibraryPaths; - for (const opt::Arg *Arg : Args.filtered(OPT_library_path)) - LibraryPaths.push_back(Arg->getValue()); - + // Build input descriptors for the archive resolver + SmallVector<offloading::InputDesc> InputDescs; bool WholeArchive = false; - SmallVector<std::pair<std::unique_ptr<MemoryBuffer>, bool>> InputFiles; for (const opt::Arg *Arg : Args.filtered( OPT_INPUT, OPT_library, OPT_whole_archive, OPT_no_whole_archive)) { if (Arg->getOption().matches(OPT_whole_archive) || @@ -512,84 +342,46 @@ Expected<SmallVector<StringRef>> getInput(const ArgList &Args) { continue; } - std::optional<std::string> Filename = - Arg->getOption().matches(OPT_library) - ? searchLibrary(Arg->getValue(), /*Root=*/"", LibraryPaths) - : std::string(Arg->getValue()); - - if (!Filename && Arg->getOption().matches(OPT_library)) - return createStringError("unable to find library -l%s", Arg->getValue()); + offloading::InputDesc Desc; + Desc.Value = Arg->getValue(); + Desc.Kind = Arg->getOption().matches(OPT_library) + ? offloading::InputDesc::Library + : offloading::InputDesc::File; + Desc.WholeArchive = WholeArchive; + InputDescs.push_back(Desc); + } - if (!Filename || !sys::fs::exists(*Filename) || - sys::fs::is_directory(*Filename)) - continue; + // Gather search paths and forced undefined symbols + SmallVector<StringRef> LibraryPaths; + for (const opt::Arg *Arg : Args.filtered(OPT_library_path)) + LibraryPaths.push_back(Arg->getValue()); - ErrorOr<std::unique_ptr<MemoryBuffer>> BufferOrErr = - MemoryBuffer::getFileOrSTDIN(*Filename); - if (std::error_code EC = BufferOrErr.getError()) - return createFileError(*Filename, EC); - - MemoryBufferRef Buffer = **BufferOrErr; - switch (identify_magic(Buffer.getBuffer())) { - case file_magic::bitcode: - case file_magic::elf_relocatable: - InputFiles.emplace_back(std::move(*BufferOrErr), /*IsLazy=*/false); - break; - case file_magic::archive: { - Expected<std::unique_ptr<object::Archive>> LibFile = - object::Archive::create(Buffer); - if (!LibFile) - return LibFile.takeError(); - Error Err = Error::success(); - for (auto Child : (*LibFile)->children(Err)) { - auto ChildBufferOrErr = Child.getMemoryBufferRef(); - if (!ChildBufferOrErr) - return ChildBufferOrErr.takeError(); - std::unique_ptr<MemoryBuffer> ChildBuffer = - MemoryBuffer::getMemBufferCopy( - ChildBufferOrErr->getBuffer(), - ChildBufferOrErr->getBufferIdentifier()); - InputFiles.emplace_back(std::move(ChildBuffer), !WholeArchive); - } - if (Err) - return Err; - break; - } - default: - return createStringError("Unsupported file type"); - } - } + std::vector<std::string> ForcedUndefStorage = Args.getAllArgValues(OPT_u); + SmallVector<StringRef> ForcedUndefs(ForcedUndefStorage.begin(), + ForcedUndefStorage.end()); - bool Extracted = true; - StringMap<Symbol> SymTab; - for (auto &Sym : Args.getAllArgValues(OPT_u)) - SymTab[Sym] = Symbol(Symbol::Undefined); - SmallVector<std::unique_ptr<MemoryBuffer>> LinkerInput; - while (Extracted) { - Extracted = false; - for (auto &[Input, IsLazy] : InputFiles) { - if (!Input) - continue; - - if (hasFatBinary(Args, *Input)) { - LinkerInput.emplace_back(std::move(Input)); - continue; - } + // Build the Inputs structure + offloading::Inputs Inputs; + Inputs.Order = InputDescs; + Inputs.SearchPaths = LibraryPaths; + Inputs.ForcedUndefs = ForcedUndefs; + Inputs.Root = ""; - // Archive members only extract if they define needed symbols. We will - // re-scan all the inputs if any files were extracted for the link job. - Expected<bool> ExtractOrErr = getSymbols(*Input, SymTab, IsLazy); - if (!ExtractOrErr) - return ExtractOrErr.takeError(); + // Create the fat binary predicate + auto IsFatBinary = [&Args](MemoryBufferRef B) -> bool { + return hasFatBinary(Args, B); + }; - Extracted |= *ExtractOrErr; - if (!*ExtractOrErr) - continue; + // Resolve archive members + Expected<offloading::ResolvedInputs> ResolvedOrErr = + offloading::resolveArchiveMembers(Inputs, IsFatBinary); + if (!ResolvedOrErr) + return ResolvedOrErr.takeError(); - LinkerInput.emplace_back(std::move(Input)); - } - } - InputFiles.clear(); + offloading::ResolvedInputs &Resolved = *ResolvedOrErr; + SmallVector<std::unique_ptr<MemoryBuffer>> LinkerInput = + std::move(Resolved.Buffers); + StringMap<offloading::Symbol> &SymTab = Resolved.SymTab; // Extract any bitcode files to be passed to the LTO pipeline. SmallVector<std::unique_ptr<MemoryBuffer>> BitcodeFiles; @@ -616,7 +408,7 @@ Expected<SmallVector<StringRef>> getInput(const ArgList &Args) { size_t Idx = 0; for (auto &Sym : Symbols) { lto::SymbolResolution &Res = Resolutions[Idx++]; - Symbol ObjSym = SymTab[Sym.getName()]; + offloading::Symbol ObjSym = SymTab[Sym.getName()]; // We will use this as the prevailing symbol in LTO if it is not // undefined and it is from the file that contained the canonical // definition. diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index e5e092c4737ec..ffd73a0fdaecf 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -18,11 +18,13 @@ #include "clang/Basic/OffloadArch.h" #include "clang/Basic/Version.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringSwitch.h" #include "llvm/BinaryFormat/Magic.h" #include "llvm/Bitcode/BitcodeWriter.h" #include "llvm/CodeGen/CommandFlags.h" +#include "llvm/Frontend/Offloading/ArchiveLinker.h" #include "llvm/Frontend/Offloading/Utility.h" #include "llvm/IR/DiagnosticPrinter.h" #include "llvm/IR/LLVMContext.h" @@ -187,26 +189,59 @@ static Error executeCommands(StringRef ExecutablePath, return Error::success(); } -static Expected<SmallVector<std::string>> getInput(const ArgList &Args) { - // Collect all input bitcode files to be passed to the linking stage. - SmallVector<std::string> BitcodeFiles; - auto Inputs = Args.filtered(OPT_INPUT); - if (Inputs.empty()) - return createStringError("No input files provided"); - for (const opt::Arg *Arg : Inputs) { - StringRef Filename = Arg->getValue(); - if (!sys::fs::exists(Filename) || sys::fs::is_directory(Filename)) - return createStringError("Input file '" + Filename + "' does not exist"); - file_magic Magic; - if (auto EC = identify_magic(Filename, Magic)) - return createStringError("Failed to open file " + Filename); - // TODO: Current use case involves LLVM IR bitcode files as input. - // This will be extended to support SPIR-V IR files. - if (Magic != file_magic::bitcode) - return createStringError("Unsupported file type for '" + Filename + "'"); - BitcodeFiles.push_back(std::string(Filename)); +static Expected<SmallVector<std::unique_ptr<MemoryBuffer>>> +getInput(const ArgList &Args) { + // Build input descriptors for the shared archive resolver + SmallVector<offloading::InputDesc> InputDescs; + bool WholeArchive = false; + for (const opt::Arg *Arg : Args.filtered( + OPT_INPUT, OPT_library, OPT_whole_archive, OPT_no_whole_archive)) { + if (Arg->getOption().matches(OPT_whole_archive) || + Arg->getOption().matches(OPT_no_whole_archive)) { + WholeArchive = Arg->getOption().matches(OPT_whole_archive); + continue; + } + + offloading::InputDesc Desc; + Desc.Value = Arg->getValue(); + Desc.Kind = Arg->getOption().matches(OPT_library) + ? offloading::InputDesc::Library + : offloading::InputDesc::File; + Desc.WholeArchive = WholeArchive; + InputDescs.push_back(Desc); } - return BitcodeFiles; + + if (InputDescs.empty()) + return createStringError(inconvertibleErrorCode(), + "No input files provided"); + + // Gather search paths and forced undefined symbols + SmallVector<StringRef> LibraryPaths; + for (const opt::Arg *Arg : Args.filtered(OPT_library_path)) + LibraryPaths.push_back(Arg->getValue()); + + std::vector<std::string> ForcedUndefStorage = Args.getAllArgValues(OPT_u); + SmallVector<StringRef> ForcedUndefs(ForcedUndefStorage.begin(), + ForcedUndefStorage.end()); + + // Build the Inputs structure + offloading::Inputs Inputs; + Inputs.Order = InputDescs; + Inputs.SearchPaths = LibraryPaths; + Inputs.ForcedUndefs = ForcedUndefs; + Inputs.Root = ""; + + // Resolve archive members (no fat binary predicate for SYCL) + Expected<offloading::ResolvedInputs> ResolvedOrErr = + offloading::resolveArchiveMembers(Inputs); + if (!ResolvedOrErr) + return ResolvedOrErr.takeError(); + + if (ResolvedOrErr->Buffers.empty()) + return createStringError(inconvertibleErrorCode(), + "No input files could be resolved"); + + return std::move(ResolvedOrErr->Buffers); } /// Handle cases where input file is a LLVM IR bitcode file. @@ -283,12 +318,15 @@ struct LinkResult { /// 3. Gather all library bitcode images. /// 4. Link all the images gathered in Step 3 with the output of Step 2 using /// linkInModule API. LinkOnlyNeeded flag is used. -static Expected<LinkResult> linkInputs(ArrayRef<std::string> InputFiles, - const ArgList &Args, LLVMContext &C) { +static Expected<LinkResult> +linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, + const ArgList &Args, LLVMContext &C) { llvm::TimeTraceScope TimeScope("Link code"); - assert(InputFiles.size() && "No inputs to link"); + assert(InputBuffers.size() && "No inputs to link"); + // TODO: Drop --bc-library in favor of the -l / .a archive path once it is + // established. // Get all library files. Expected<SmallVector<std::string>> BCLibFiles = getBCLibraryNames(Args); if (!BCLibFiles) @@ -301,7 +339,10 @@ static Expected<LinkResult> linkInputs(ArrayRef<std::string> InputFiles, return BitcodeOutput.takeError(); if (Verbose) { - std::string Inputs = llvm::join(InputFiles.begin(), InputFiles.end(), ", "); + std::string Inputs = llvm::join( + llvm::map_range(InputBuffers, + [](const auto &B) { return B->getBufferIdentifier(); }), + ", "); std::string LibInputs = llvm::join((*BCLibFiles).begin(), (*BCLibFiles).end(), ", "); errs() << formatv("link: inputs: {0} libfiles: {1} output: {2}\n", Inputs, @@ -314,8 +355,11 @@ static Expected<LinkResult> linkInputs(ArrayRef<std::string> InputFiles, auto LinkerOutput = std::make_unique<Module>("linker-output", C); Linker L(*LinkerOutput); - for (auto &File : InputFiles) { - auto ModOrErr = getBitcodeModule(File, C); + for (const auto &Buffer : InputBuffers) { + // Data is already in memory; use eager parse (unlike getBitcodeModule which + // stays lazy for --bc-library files where LinkOnlyNeeded skips most + // bodies). + auto ModOrErr = parseBitcodeFile(Buffer->getMemBufferRef(), C); if (!ModOrErr) return ModOrErr.takeError(); @@ -323,20 +367,23 @@ static Expected<LinkResult> linkInputs(ArrayRef<std::string> InputFiles, if (!T.empty() && T != TargetTriple) { if (TargetTriple.empty()) { TargetTriple = T; - TripleSource = File; + TripleSource = Buffer->getBufferIdentifier(); } else { return createStringError( + inconvertibleErrorCode(), "conflicting target triples: '" + TargetTriple.str() + "' (from " + - TripleSource + ") vs '" + T.str() + "' (from " + File + ")"); + TripleSource + ") vs '" + T.str() + "' (from " + + Buffer->getBufferIdentifier() + ")"); } } if (L.linkInModule(std::move(*ModOrErr))) - return createStringError("Could not link IR"); + return createStringError(inconvertibleErrorCode(), "Could not link IR"); } if (TargetTriple.empty()) return createStringError( + inconvertibleErrorCode(), "Target triple must be specified or inferable from inputs"); // Link in library files. @@ -347,7 +394,7 @@ static Expected<LinkResult> linkInputs(ArrayRef<std::string> InputFiles, if ((*LibMod)->getTargetTriple() == TargetTriple) { unsigned Flags = Linker::Flags::LinkOnlyNeeded; if (L.linkInModule(std::move(*LibMod), Flags)) - return createStringError("Could not link IR"); + return createStringError(inconvertibleErrorCode(), "Could not link IR"); } } @@ -693,13 +740,14 @@ static bool canSkipModuleSplit(IRSplitMode Mode, const Module &M, /// 4. Optionally run AOT compilation when targeting an Intel HW arch. /// 5. Pack the resulting images into a single OffloadBinary written to the /// output file. -static Error runSYCLLink(ArrayRef<std::string> Files, const ArgList &Args) { +static Error runSYCLLink(ArrayRef<std::unique_ptr<MemoryBuffer>> Buffers, + const ArgList &Args) { llvm::TimeTraceScope TimeScope("SYCL linking"); LLVMContext C; // Link all input bitcode files and library files. - Expected<LinkResult> LinkedOrErr = linkInputs(Files, Args, C); + Expected<LinkResult> LinkedOrErr = linkInputs(Buffers, Args, C); if (!LinkedOrErr) return LinkedOrErr.takeError(); LinkResult &Result = *LinkedOrErr; diff --git a/clang/tools/clang-sycl-linker/SYCLLinkOpts.td b/clang/tools/clang-sycl-linker/SYCLLinkOpts.td index e00e63aa1767d..9d1a3347ac0fd 100644 --- a/clang/tools/clang-sycl-linker/SYCLLinkOpts.td +++ b/clang/tools/clang-sycl-linker/SYCLLinkOpts.td @@ -24,6 +24,22 @@ def library_path_S : Separate<["--", "-"], "library-path">, Flags<[HelpHidden]>, def library_path_EQ : Joined<["--", "-"], "library-path=">, Flags<[HelpHidden]>, Alias<library_path>; +def library : JoinedOrSeparate<["-"], "l">, MetaVarName<"<libname>">, + HelpText<"Search for library <libname>">; +def library_S : Separate<["--", "-"], "library">, Flags<[HelpHidden]>, + Alias<library>; +def library_EQ : Joined<["--", "-"], "library=">, Flags<[HelpHidden]>, + Alias<library>; + +def whole_archive : Flag<["--", "-"], "whole-archive">, + HelpText<"Include all archive members in the link">; +def no_whole_archive : Flag<["--", "-"], "no-whole-archive">, + HelpText<"Only include archive members that resolve undefined symbols (default)">; + +def u : JoinedOrSeparate<["-"], "u">, MetaVarName<"<symbol>">, + HelpText<"Force undefined symbol during linking">; +def undefined : JoinedOrSeparate<["--"], "undefined">, Alias<u>; + def bc_library : Separate<["--", "-"], "bc-library">, MetaVarName<"<name>">, HelpText<"Add LLVM bitcode library <name> (with extension) to the link. A " "relative <name> is resolved against -L paths; an absolute path is " diff --git a/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h b/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h new file mode 100644 index 0000000000000..8594b0bfb2ba0 --- /dev/null +++ b/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h @@ -0,0 +1,115 @@ +//===- ArchiveLinker.h - Archive member selection for offloading -*- 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 +// +//===----------------------------------------------------------------------===// +// +// This file declares shared functionality for linking static libraries +// (archives) in offloading tools. It provides a symbol-driven fixed-point +// archive member selection algorithm used by both clang-nvlink-wrapper and +// clang-sycl-linker. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_FRONTEND_OFFLOADING_ARCHIVELINKER_H +#define LLVM_FRONTEND_OFFLOADING_ARCHIVELINKER_H + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Object/IRSymtab.h" +#include "llvm/Object/SymbolicFile.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/MemoryBufferRef.h" +#include <functional> +#include <memory> + +namespace llvm { +class MemoryBuffer; + +namespace object { +class SymbolRef; +} // namespace object + +namespace offloading { + +/// A minimum symbol interface that provides the necessary information to +/// extract archive members and resolve LTO symbols. +struct Symbol { + enum Flags { + None = 0, + Undefined = 1 << 0, + Weak = 1 << 1, + }; + + Symbol() : File(), SymFlags(None), UsedInRegularObj(false) {} + Symbol(Symbol::Flags F) : File(), SymFlags(F), UsedInRegularObj(true) {} + + Symbol(MemoryBufferRef File, const irsymtab::Reader::SymbolRef Sym) + : File(File), SymFlags(0), UsedInRegularObj(false) { + if (Sym.isUndefined()) + SymFlags |= Undefined; + if (Sym.isWeak()) + SymFlags |= Weak; + } + + /// Create a Symbol from an object file symbol reference. + /// Returns an error if symbol flags cannot be retrieved. + static Expected<Symbol> createFromObject(MemoryBufferRef File, + const object::SymbolRef &Sym); + + bool isWeak() const { return SymFlags & Weak; } + bool isUndefined() const { return SymFlags & Undefined; } + + MemoryBufferRef File; + uint32_t SymFlags; + bool UsedInRegularObj; +}; + +/// Description of a single input (file or library). +struct InputDesc { + StringRef Value; // file path, or library name for -l (the value after -l) + enum KindTy { File, Library } Kind; + bool WholeArchive; // --whole-archive state in effect at this input +}; + +/// All inputs and search paths for archive member resolution. +struct Inputs { + ArrayRef<InputDesc> Order; // positional inputs + -l libraries in order + ArrayRef<StringRef> SearchPaths; // -L paths + ArrayRef<StringRef> ForcedUndefs; // -u symbols (may be empty) + StringRef Root; // sysroot for "=" prefixed paths ("" if none) +}; + +/// Result of archive member resolution. +struct ResolvedInputs { + SmallVector<std::unique_ptr<MemoryBuffer>> + Buffers; // members to link, in order + StringMap<Symbol> SymTab; // symbol table (for LTO resolution) +}; + +/// Resolve archive members from the given inputs using a symbol-driven +/// fixed-point algorithm. For each input: +/// - If it's a Library, search for lib<name>.a or :<name> in SearchPaths +/// - If it's a File, use the path directly +/// - Archives are expanded and members are lazily extracted based on symbol +/// references unless WholeArchive is true +/// - Non-archive inputs (bitcode, ELF objects) are always included +/// +/// Returns the buffers to link and the symbol table for LTO resolution. +/// +/// \param In The inputs to resolve +/// \param IsFatBinary Optional predicate to identify "fat binary" inputs that +/// should be passed through without symbol scanning (e.g., nvlink's +/// cubin detection). If null, all inputs are scanned normally. +Expected<ResolvedInputs> resolveArchiveMembers( + const Inputs &In, + function_ref<bool(MemoryBufferRef)> IsFatBinary = nullptr); + +} // namespace offloading +} // namespace llvm + +#endif // LLVM_FRONTEND_OFFLOADING_ARCHIVELINKER_H diff --git a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp new file mode 100644 index 0000000000000..e861fbe0a9927 --- /dev/null +++ b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp @@ -0,0 +1,268 @@ +//===- ArchiveLinker.cpp - Archive member selection for offloading --------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// This file implements shared functionality for linking static libraries +// (archives) in offloading tools. +// +//===----------------------------------------------------------------------===// + +#include "llvm/Frontend/Offloading/ArchiveLinker.h" +#include "llvm/BinaryFormat/Magic.h" +#include "llvm/Object/Archive.h" +#include "llvm/Object/IRObjectFile.h" +#include "llvm/Object/ObjectFile.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" + +using namespace llvm; +using namespace llvm::object; + +namespace llvm { +namespace offloading { + +Expected<Symbol> Symbol::createFromObject(MemoryBufferRef File, + const SymbolRef &Sym) { + Symbol Result; + Result.File = File; + + auto FlagsOrErr = Sym.getFlags(); + if (!FlagsOrErr) + return FlagsOrErr.takeError(); + + if (*FlagsOrErr & SymbolRef::SF_Undefined) + Result.SymFlags |= Undefined; + if (*FlagsOrErr & SymbolRef::SF_Weak) + Result.SymFlags |= Weak; + + return Result; +} + +static std::optional<std::string> findFile(StringRef Dir, StringRef Root, + const Twine &Name) { + SmallString<128> Path; + if (Dir.starts_with("=")) + sys::path::append(Path, Root, Dir.substr(1), Name); + else + sys::path::append(Path, Dir, Name); + + if (sys::fs::exists(Path)) + return static_cast<std::string>(Path); + return std::nullopt; +} + +static std::optional<std::string> +findFromSearchPaths(StringRef Name, StringRef Root, + ArrayRef<StringRef> SearchPaths) { + for (StringRef Dir : SearchPaths) + if (std::optional<std::string> File = findFile(Dir, Root, Name)) + return File; + return std::nullopt; +} + +/// Search for static libraries in the linker's library path given input like +/// `-lfoo` or `-l:libfoo.a`. +static std::optional<std::string> +searchLibrary(StringRef Input, StringRef Root, + ArrayRef<StringRef> SearchPaths) { + if (Input.starts_with(":")) + return findFromSearchPaths(Input.drop_front(), Root, SearchPaths); + SmallString<128> LibName; + ("lib" + Input + ".a").toVector(LibName); + return findFromSearchPaths(LibName, Root, SearchPaths); +} + +static Expected<bool> getSymbolsFromBitcode(MemoryBufferRef Buffer, + StringMap<Symbol> &SymTab, + bool IsLazy) { + Expected<IRSymtabFile> IRSymtabOrErr = readIRSymtab(Buffer); + if (!IRSymtabOrErr) + return IRSymtabOrErr.takeError(); + bool Extracted = !IsLazy; + StringMap<Symbol> PendingSymbols; + for (unsigned I = 0; I != IRSymtabOrErr->Mods.size(); ++I) { + for (const auto &IRSym : IRSymtabOrErr->TheReader.module_symbols(I)) { + if (IRSym.isFormatSpecific() || !IRSym.isGlobal()) + continue; + + StringMap<Symbol> &Target = + (IsLazy && !SymTab.count(IRSym.getName())) ? PendingSymbols : SymTab; + Symbol &OldSym = Target[IRSym.getName()]; + Symbol Sym = Symbol(Buffer, IRSym); + if (OldSym.File.getBuffer().empty()) + OldSym = Sym; + + bool ResolvesReference = + !Sym.isUndefined() && + (OldSym.isUndefined() || (OldSym.isWeak() && !Sym.isWeak())) && + !(OldSym.isWeak() && OldSym.isUndefined() && IsLazy); + Extracted |= ResolvesReference; + + Sym.UsedInRegularObj = OldSym.UsedInRegularObj; + if (ResolvesReference) + OldSym = Sym; + } + } + if (Extracted) + for (const auto &[Name, Symbol] : PendingSymbols) + SymTab[Name] = Symbol; + return Extracted; +} + +static Expected<bool> getSymbolsFromObject(ObjectFile &ObjFile, + StringMap<Symbol> &SymTab, + bool IsLazy) { + bool Extracted = !IsLazy; + StringMap<Symbol> PendingSymbols; + for (SymbolRef ObjSym : ObjFile.symbols()) { + auto NameOrErr = ObjSym.getName(); + if (!NameOrErr) + return NameOrErr.takeError(); + + StringMap<Symbol> &Target = + (IsLazy && !SymTab.count(*NameOrErr)) ? PendingSymbols : SymTab; + Symbol &OldSym = Target[*NameOrErr]; + + auto SymOrErr = + Symbol::createFromObject(ObjFile.getMemoryBufferRef(), ObjSym); + if (!SymOrErr) + return SymOrErr.takeError(); + Symbol Sym = *SymOrErr; + + if (OldSym.File.getBuffer().empty()) + OldSym = Sym; + + bool ResolvesReference = OldSym.isUndefined() && !Sym.isUndefined() && + (!OldSym.isWeak() || !IsLazy); + Extracted |= ResolvesReference; + + if (ResolvesReference) + OldSym = Sym; + OldSym.UsedInRegularObj = true; + } + if (Extracted) + for (const auto &[Name, Symbol] : PendingSymbols) + SymTab[Name] = Symbol; + return Extracted; +} + +static Expected<bool> getSymbols(MemoryBufferRef Buffer, + StringMap<Symbol> &SymTab, bool IsLazy) { + switch (identify_magic(Buffer.getBuffer())) { + case file_magic::bitcode: { + return getSymbolsFromBitcode(Buffer, SymTab, IsLazy); + } + case file_magic::elf_relocatable: { + Expected<std::unique_ptr<ObjectFile>> ObjFile = + ObjectFile::createObjectFile(Buffer); + if (!ObjFile) + return ObjFile.takeError(); + return getSymbolsFromObject(**ObjFile, SymTab, IsLazy); + } + default: + return createStringError("Unsupported file type"); + } +} + +Expected<ResolvedInputs> +resolveArchiveMembers(const Inputs &In, + function_ref<bool(MemoryBufferRef)> IsFatBinary) { + ResolvedInputs Result; + SmallVector<std::pair<std::unique_ptr<MemoryBuffer>, bool>> InputFiles; + + // Process each input descriptor + for (const InputDesc &Desc : In.Order) { + std::optional<std::string> Filename; + + if (Desc.Kind == InputDesc::Library) { + Filename = searchLibrary(Desc.Value, In.Root, In.SearchPaths); + if (!Filename) + return createStringError("unable to find library -l%s", + Desc.Value.str().c_str()); + } else { + if (!sys::fs::exists(Desc.Value) || sys::fs::is_directory(Desc.Value)) + continue; + Filename = Desc.Value.str(); + } + + if (!Filename) + continue; + + auto BufferOrErr = + errorOrToExpected(MemoryBuffer::getFileOrSTDIN(*Filename)); + if (!BufferOrErr) + return createFileError(*Filename, BufferOrErr.takeError()); + + MemoryBufferRef Buffer = (*BufferOrErr)->getMemBufferRef(); + switch (identify_magic(Buffer.getBuffer())) { + case file_magic::bitcode: + case file_magic::elf_relocatable: + InputFiles.emplace_back(std::move(*BufferOrErr), /*IsLazy=*/false); + break; + case file_magic::archive: { + Expected<std::unique_ptr<object::Archive>> LibFile = + object::Archive::create(Buffer); + if (!LibFile) + return LibFile.takeError(); + Error Err = Error::success(); + for (auto Child : (*LibFile)->children(Err)) { + auto ChildBufferOrErr = Child.getMemoryBufferRef(); + if (!ChildBufferOrErr) + return ChildBufferOrErr.takeError(); + std::unique_ptr<MemoryBuffer> ChildBuffer = + MemoryBuffer::getMemBufferCopy( + ChildBufferOrErr->getBuffer(), + ChildBufferOrErr->getBufferIdentifier()); + InputFiles.emplace_back(std::move(ChildBuffer), !Desc.WholeArchive); + } + if (Err) + return Err; + break; + } + default: + return createStringError("Unsupported file type"); + } + } + + // Seed symbol table with forced undefined symbols + for (StringRef Sym : In.ForcedUndefs) + Result.SymTab[Sym] = Symbol(Symbol::Undefined); + + // Fixed-point loop to extract archive members + bool Extracted = true; + while (Extracted) { + Extracted = false; + for (auto &[Input, IsLazy] : InputFiles) { + if (!Input) + continue; + + // Check if this is a fat binary that should be passed through + if (IsFatBinary && IsFatBinary(*Input)) { + Result.Buffers.emplace_back(std::move(Input)); + continue; + } + + // Archive members only extract if they define needed symbols + Expected<bool> ExtractOrErr = getSymbols(*Input, Result.SymTab, IsLazy); + if (!ExtractOrErr) + return ExtractOrErr.takeError(); + + Extracted |= *ExtractOrErr; + if (!*ExtractOrErr) + continue; + + Result.Buffers.emplace_back(std::move(Input)); + } + } + + return Result; +} + +} // namespace offloading +} // namespace llvm diff --git a/llvm/lib/Frontend/Offloading/CMakeLists.txt b/llvm/lib/Frontend/Offloading/CMakeLists.txt index 9747dbde043da..82c49018b9bf3 100644 --- a/llvm/lib/Frontend/Offloading/CMakeLists.txt +++ b/llvm/lib/Frontend/Offloading/CMakeLists.txt @@ -1,4 +1,5 @@ add_llvm_component_library(LLVMFrontendOffloading + ArchiveLinker.cpp Utility.cpp OffloadWrapper.cpp PropertySet.cpp >From 0be366d099ef600477856994b8c1611d09d93992 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 4 Jun 2026 11:07:19 -0700 Subject: [PATCH 2/9] [clang][sycl-linker] Remove --bc-library option in favor of -l Remove the --bc-library option and use the standard -l option with archive libraries instead. Device library bitcode files should now be packaged into .a archives using llvm-ar and linked via -l. Key changes: - Removed --bc-library and --bc-library= option definitions - Removed getBCLibraryNames(), getBitcodeModule(), and searchLibrary() - Simplified linkInputs() to use archive linker infrastructure - Updated verbose output format (removed separate libfiles field) Archive linking behavior: - Use -l <name> to search for lib<name>.a in -L paths - Use -l :<exact-name> to search for exact filename in -L paths - Use --whole-archive to force extraction of all archive members - Lazy linking operates at archive member (file) granularity Updated tests to: - Create .a archives from .bc files using llvm-ar - Use -l option with --whole-archive where needed - Verify lazy archive linking (unused members not extracted) - Verify search path order and error messages include filenames Co-Authored-By: Claude Sonnet 4.5 <[email protected]> --- .../OffloadTools/clang-sycl-linker/basic.ll | 75 ++++++++++------ .../OffloadTools/clang-sycl-linker/link.ll | 54 +++++++++-- .../clang-sycl-linker/split-mode.ll | 6 +- .../clang-sycl-linker/ClangSYCLLinker.cpp | 89 +------------------ clang/tools/clang-sycl-linker/SYCLLinkOpts.td | 7 -- 5 files changed, 102 insertions(+), 129 deletions(-) diff --git a/clang/test/OffloadTools/clang-sycl-linker/basic.ll b/clang/test/OffloadTools/clang-sycl-linker/basic.ll index 33e6181ed3874..44d344657de3c 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/basic.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/basic.ll @@ -24,39 +24,46 @@ ; Test that IMG_SPIRV image kind is set for non-AOT compilation. ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc -o %t/spirv.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=SIMPLE-FO -; SIMPLE-FO: link: inputs: {{.*}}.bc, {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; SIMPLE-FO: link: inputs: {{.*}}.bc, {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; SIMPLE-FO-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: {{.*}}_0.spv ; SIMPLE-FO-NEXT: sycl-bundle: image kind: spv, triple: spirv64, arch: {{$}} ; SIMPLE-FO-NOT: {{.+}} ; -; Test the dry run of a simple case with device library files specified. +; Test the dry run of a simple case with device library archive specified using --whole-archive. ; RUN: mkdir -p %t/libs -; RUN: touch %t/libs/lib1.bc -; RUN: touch %t/libs/lib2.bc -; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc --library-path=%t/libs --bc-library lib1.bc --bc-library lib2.bc -o a.spv 2>&1 \ +; RUN: llvm-as %t/lib1.ll -o %t/libs/lib1.bc +; RUN: llvm-as %t/lib2.ll -o %t/libs/lib2.bc +; RUN: rm -f %t/libs/libdevice.a +; RUN: llvm-ar rc %t/libs/libdevice.a %t/libs/lib1.bc %t/libs/lib2.bc +; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc --library-path=%t/libs --whole-archive -l device -o a.spv 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBS -; DEVLIBS: link: inputs: {{.*}}.bc libfiles: {{.*}}lib1.bc, {{.*}}lib2.bc output: [[LLVMLINKOUT:.*]].bc +; DEVLIBS: link: inputs: {{.*}}.bc, {{.*}}.bc, lib1.bc, lib2.bc output: [[LLVMLINKOUT:.*]].bc ; DEVLIBS-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: a_0.spv ; DEVLIBS-NEXT: sycl-bundle: image kind: spv, triple: spirv64, arch: {{$}} ; DEVLIBS-NOT: {{.+}} ; -; Test -L short form (joined) and --bc-library= joined form. -; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L%t/libs --bc-library=lib1.bc -o a.spv 2>&1 \ +; Test -L short form (joined) and -l with archive using --whole-archive. +; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L%t/libs --whole-archive -l device -o a.spv 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBS-SHORT -; DEVLIBS-SHORT: link: inputs: {{.*}}.bc libfiles: {{.*}}libs{{[/\\]}}lib1.bc output: {{.*}}.bc +; DEVLIBS-SHORT: link: inputs: {{.*}}.bc, lib1.bc, lib2.bc output: {{.*}}.bc ; -; Test that search continues past the first -L when the library is not found there. lib1.bc exists only in %t/libs (the second -L). +; Test that search continues past the first -L when the library is not found there. libdevice.a exists only in %t/libs (the second -L). ; RUN: mkdir -p %t/empty -; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L %t/empty -L %t/libs --bc-library lib1.bc -o a.spv 2>&1 \ +; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L %t/empty -L %t/libs --whole-archive -l device -o a.spv 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBS-FALLTHROUGH -; DEVLIBS-FALLTHROUGH: link: inputs: {{.*}}.bc libfiles: {{.*}}libs{{[/\\]}}lib1.bc output: {{.*}}.bc +; DEVLIBS-FALLTHROUGH: link: inputs: {{.*}}.bc, lib1.bc, lib2.bc output: {{.*}}.bc ; ; Test that -L paths are searched in order: when the same name exists in multiple -L dirs, the first one wins. ; RUN: mkdir -p %t/libs2 -; RUN: touch %t/libs/shadow.bc %t/libs2/shadow.bc -; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L %t/libs2 -L %t/libs --bc-library shadow.bc -o a.spv 2>&1 \ +; RUN: llvm-as %t/lib1.ll -o %t/libs/shadow.bc +; RUN: llvm-as %t/lib2.ll -o %t/libs2/shadow.bc +; RUN: rm -f %t/libs/libshadow.a %t/libs2/libshadow.a +; RUN: llvm-ar rc %t/libs/libshadow.a %t/libs/shadow.bc +; RUN: llvm-ar rc %t/libs2/libshadow.a %t/libs2/shadow.bc +; RUN: clang-sycl-linker --dry-run --module-split-mode=none %t/input1.bc -L %t/libs2 -L %t/libs --whole-archive -l shadow -o a.spv --print-linked-module 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBS-ORDER -; DEVLIBS-ORDER: link: inputs: {{.*}}.bc libfiles: {{.*}}libs2{{[/\\]}}shadow.bc output: {{.*}}.bc +; DEVLIBS-ORDER: define {{.*}}lib2_func +; DEVLIBS-ORDER-NOT: define {{.*}}lib1_func ; ; Test a simple case with a random file (not bitcode) as input. ; RUN: touch %t/dummy.o @@ -65,29 +72,29 @@ ; FILETYPEERROR: Unsupported file type ; ; Test to see if device library related errors are emitted. -; RUN: not clang-sycl-linker --dry-run %t/input1.bc %t/input2.bc --library-path=%t/libs --bc-library lib1.bc --bc-library lib2.bc --bc-library lib3.bc -o a.spv 2>&1 \ +; RUN: not clang-sycl-linker --dry-run %t/input1.bc %t/input2.bc --library-path=%t/libs -l device -l nonexistent -o a.spv 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBSERR -; DEVLIBSERR: '{{.*}}lib3.bc' library file not found +; DEVLIBSERR: unable to find library -lnonexistent ; -; Test that there is no implicit CWD search: a bare bitcode name without any -L +; Test that there is no implicit CWD search: a bare library name without any -L ; must fail to resolve, even if a same-named file exists in the CWD. -; RUN: cd %t && not clang-sycl-linker --dry-run input1.bc --bc-library input1.bc -o a.spv 2>&1 \ +; RUN: cd %t && not clang-sycl-linker --dry-run input1.bc -l input1 -o a.spv 2>&1 \ ; RUN: | FileCheck %s --check-prefix=NO-CWD-SEARCH -; NO-CWD-SEARCH: 'input1.bc' library file not found +; NO-CWD-SEARCH: unable to find library -linput1 ; ; Test that a directory matching the requested name is not accepted as a library: -; %t/libs is a directory created above; resolving --bc-library libs against -L %t -; would otherwise pick it up and fail later with a confusing bitcode-reader error. -; RUN: not clang-sycl-linker --dry-run %t/input1.bc -L %t --bc-library libs -o a.spv 2>&1 \ +; %t/libs is a directory created above; resolving -l:libs against -L %t +; would detect it's a directory and error with the filename in the message. +; RUN: not clang-sycl-linker --dry-run %t/input1.bc -L %t -l :libs -o a.spv 2>&1 \ ; RUN: | FileCheck %s --check-prefix=NO-DIR-AS-LIB -; NO-DIR-AS-LIB: 'libs' library file not found +; NO-DIR-AS-LIB: '{{.*}}libs': Is a directory ; ; Test AOT compilation for an Intel GPU. ; Test that IMG_Object image kind is set for AOT compilation (Intel GPU). ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none -arch=bmg_g21 %t/input1.bc %t/input2.bc -o %t/aot-gpu.out 2>&1 \ ; RUN: --ocloc-options="-a -b" \ ; RUN: | FileCheck %s --check-prefix=AOT-INTEL-GPU -; AOT-INTEL-GPU: link: inputs: {{.*}}.bc, {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; AOT-INTEL-GPU: link: inputs: {{.*}}.bc, {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; AOT-INTEL-GPU-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: [[SPIRVTRANSLATIONOUT:.*]]_0.spv ; AOT-INTEL-GPU-NEXT: "{{.*}}ocloc{{.*}}" {{.*}}-device bmg_g21 -a -b {{.*}}-output [[SPIRVTRANSLATIONOUT]]_0.out -file [[SPIRVTRANSLATIONOUT]]_0.spv ; AOT-INTEL-GPU-NEXT: sycl-bundle: image kind: o, triple: spirv64, arch: bmg_g21 @@ -98,7 +105,7 @@ ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none -arch=graniterapids %t/input1.bc %t/input2.bc -o %t/aot-cpu.out 2>&1 \ ; RUN: --opencl-aot-options="-a -b" \ ; RUN: | FileCheck %s --check-prefix=AOT-INTEL-CPU -; AOT-INTEL-CPU: link: inputs: {{.*}}.bc, {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; AOT-INTEL-CPU: link: inputs: {{.*}}.bc, {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; AOT-INTEL-CPU-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: [[SPIRVTRANSLATIONOUT:.*]]_0.spv ; AOT-INTEL-CPU-NEXT: "{{.*}}opencl-aot{{.*}}" {{.*}}--device=cpu -a -b {{.*}}-o [[SPIRVTRANSLATIONOUT]]_0.out [[SPIRVTRANSLATIONOUT]]_0.spv ; AOT-INTEL-CPU-NEXT: sycl-bundle: image kind: o, triple: spirv64, arch: graniterapids @@ -160,3 +167,19 @@ target triple = "spirv64" define spir_func i32 @helper() { ret i32 0 } + +;--- lib1.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @lib1_func() { + ret i32 1 +} + +;--- lib2.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @lib2_func() { + ret i32 2 +} diff --git a/clang/test/OffloadTools/clang-sycl-linker/link.ll b/clang/test/OffloadTools/clang-sycl-linker/link.ll index 4114f0a3f3fb1..3bde153f833cf 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/link.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/link.ll @@ -7,6 +7,11 @@ ; RUN: llvm-as %t/bar.ll -o %t/bar.bc ; RUN: llvm-as %t/baz.ll -o %t/baz.bc ; RUN: llvm-as %t/libfoo.ll -o %t/libfoo.bc +; RUN: llvm-as %t/addFive.ll -o %t/addFive.bc +; RUN: llvm-as %t/unusedFunc.ll -o %t/unusedFunc.bc +; RUN: rm -f %t/libfoo.a %t/libdevice.a +; RUN: llvm-ar rc %t/libfoo.a %t/libfoo.bc +; RUN: llvm-ar rc %t/libdevice.a %t/addFive.bc %t/unusedFunc.bc ; ; Test linking two input files. ; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc --dry-run -o a.spv --print-linked-module 2>&1 \ @@ -22,18 +27,35 @@ ; RUN: | FileCheck %s --check-prefix=CHECK-MULTIPLE-DEFS ; CHECK-MULTIPLE-DEFS: error: Linking globals named {{.*}}bar_func1{{.*}} symbol multiply defined! ; -; Test linking with a BC library file resolved through -L. -; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc --bc-library libfoo.bc -L %t --dry-run -o a.spv --print-linked-module 2>&1 \ +; Test lazy linking with an archive library: only needed members are extracted. +; foo.bc references addFive, so addFive.bc is extracted from libdevice.a. +; unusedFunc.bc is not needed, so it should NOT be extracted. +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc -l device -L %t --dry-run -o a.spv --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-LAZY-LINK +; CHECK-LAZY-LINK: define {{.*}}foo_func1{{.*}} +; CHECK-LAZY-LINK: define {{.*}}foo_func2{{.*}} +; CHECK-LAZY-LINK: define {{.*}}bar_func1{{.*}} +; CHECK-LAZY-LINK: define {{.*}}addFive{{.*}} +; CHECK-LAZY-LINK-NOT: define {{.*}}unusedFunc{{.*}} +; +; Test linking with an archive library file using -l:libname.a syntax. +; Archive linking extracts members at file granularity, so all functions in libfoo.bc are included. +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc -l :libfoo.a -L %t --dry-run -o a.spv --print-linked-module 2>&1 \ ; RUN: | FileCheck %s --check-prefix=CHECK-DEVICE-LIB ; CHECK-DEVICE-LIB: define {{.*}}foo_func1{{.*}} ; CHECK-DEVICE-LIB: define {{.*}}foo_func2{{.*}} ; CHECK-DEVICE-LIB: define {{.*}}bar_func1{{.*}} ; CHECK-DEVICE-LIB: define {{.*}}addFive{{.*}} -; CHECK-DEVICE-LIB-NOT: define {{.*}}unusedFunc{{.*}} +; CHECK-DEVICE-LIB: define {{.*}}unusedFunc{{.*}} ; -; Test that an absolute path to --bc-library is taken as-is, with no -L required. -; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc --bc-library %t/libfoo.bc --dry-run -o a.spv --print-linked-module 2>&1 \ -; RUN: | FileCheck %s --check-prefix=CHECK-DEVICE-LIB +; Test that an absolute path as a positional argument includes all archive members. +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc %t/libfoo.a --dry-run -o a.spv --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-DEVICE-LIB-ALL +; CHECK-DEVICE-LIB-ALL: define {{.*}}foo_func1{{.*}} +; CHECK-DEVICE-LIB-ALL: define {{.*}}foo_func2{{.*}} +; CHECK-DEVICE-LIB-ALL: define {{.*}}bar_func1{{.*}} +; CHECK-DEVICE-LIB-ALL: define {{.*}}addFive{{.*}} +; CHECK-DEVICE-LIB-ALL: define {{.*}}unusedFunc{{.*}} ;--- foo.ll target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" @@ -93,3 +115,23 @@ entry: %res = mul nsw i32 %a, 5 ret i32 %res } + +;--- addFive.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @addFive(i32 %a) { +entry: + %res = add nsw i32 %a, 5 + ret i32 %res +} + +;--- unusedFunc.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @unusedFunc(i32 %a) { +entry: + %res = mul nsw i32 %a, 5 + ret i32 %res +} diff --git a/clang/test/OffloadTools/clang-sycl-linker/split-mode.ll b/clang/test/OffloadTools/clang-sycl-linker/split-mode.ll index d10dbacf259fe..4cb2184f93e94 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/split-mode.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/split-mode.ll @@ -12,7 +12,7 @@ ; Test the split mode ("none"): kernels from different TUs are not split into separate images. ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t.bc -o %t-none.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=SPLIT-NONE -; SPLIT-NONE: link: inputs: {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; SPLIT-NONE: link: inputs: {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; SPLIT-NONE-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: {{.*}}_0.spv ; SPLIT-NONE-NEXT: sycl-bundle: image kind: spv, triple: spirv64, arch: {{$}} ; SPLIT-NONE-NOT: {{.+}} @@ -20,7 +20,7 @@ ; Test the split mode ("kernel"): each SPIR_KERNEL function produces its own device image. ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=kernel %t.bc -o %t-split-kernel.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=SPLIT-KERNEL -; SPLIT-KERNEL: link: inputs: {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; SPLIT-KERNEL: link: inputs: {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; SPLIT-KERNEL-NEXT: sycl-module-split: input: [[LLVMLINKOUT]].bc, mode: kernel ; SPLIT-KERNEL-NEXT: [[SPLIT0:.*]].bc [kernel_c ] ; SPLIT-KERNEL-NEXT: [[SPLIT1:.*]].bc [kernel_b ] @@ -43,7 +43,7 @@ ; Test per-TU split ('source' explicitly provided) ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=source %t.bc -o %t-src.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=SPLIT-SRC -; SPLIT-SRC: link: inputs: {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; SPLIT-SRC: link: inputs: {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; SPLIT-SRC-NEXT: sycl-module-split: input: [[LLVMLINKOUT]].bc, mode: source ; SPLIT-SRC-NEXT: [[S0:.*]].bc [kernel_b kernel_c ] ; SPLIT-SRC-NEXT: [[S1:.*]].bc [kernel_a ] diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index ffd73a0fdaecf..98ef9c09cb135 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -244,64 +244,6 @@ getInput(const ArgList &Args) { return std::move(ResolvedOrErr->Buffers); } -/// Handle cases where input file is a LLVM IR bitcode file. -/// When clang-sycl-linker is called via clang-linker-wrapper tool, input files -/// are LLVM IR bitcode files. -// TODO: Support SPIR-V IR files. -static Expected<std::unique_ptr<Module>> getBitcodeModule(StringRef File, - LLVMContext &C) { - SMDiagnostic Err; - - auto M = getLazyIRFileModule(File, Err, C); - if (M) - return std::move(M); - return createStringError(Err.getMessage()); -} - -static std::optional<std::string> findFile(StringRef Dir, const Twine &Name) { - SmallString<128> Path(Dir); - llvm::sys::path::append(Path, Name); - if (sys::fs::exists(Path) && !sys::fs::is_directory(Path)) - return std::string(Path); - return std::nullopt; -} - -static std::optional<std::string> -searchLibrary(StringRef Name, ArrayRef<StringRef> SearchPaths) { - // An absolute path is taken as-is; -L paths are only consulted for relative - // names. - if (sys::path::is_absolute(Name)) { - if (sys::fs::exists(Name) && !sys::fs::is_directory(Name)) - return std::string(Name); - return std::nullopt; - } - for (StringRef Dir : SearchPaths) - if (std::optional<std::string> File = findFile(Dir, Name)) - return File; - return std::nullopt; -} - -/// Gather all library files. The list of files and its location are passed from -/// driver. -static Expected<SmallVector<std::string>> -getBCLibraryNames(const ArgList &Args) { - SmallVector<StringRef> LibraryPaths; - for (const opt::Arg *Arg : Args.filtered(OPT_library_path)) - LibraryPaths.push_back(Arg->getValue()); - - SmallVector<std::string> LibraryFiles; - for (const opt::Arg *Arg : Args.filtered(OPT_bc_library)) { - std::optional<std::string> LibName = - searchLibrary(Arg->getValue(), LibraryPaths); - if (!LibName) - return createStringError("'" + Twine(Arg->getValue()) + - "' library file not found"); - LibraryFiles.push_back(std::move(*LibName)); - } - - return LibraryFiles; -} - namespace { struct LinkResult { std::unique_ptr<Module> LinkedModule; @@ -315,9 +257,6 @@ struct LinkResult { /// first input that supplies a triple as canonical. Issue an error if any /// triple inputs disagree. /// 2. Link all input bitcode images into one image using the linkInModule API. -/// 3. Gather all library bitcode images. -/// 4. Link all the images gathered in Step 3 with the output of Step 2 using -/// linkInModule API. LinkOnlyNeeded flag is used. static Expected<LinkResult> linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, const ArgList &Args, LLVMContext &C) { @@ -325,13 +264,6 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, assert(InputBuffers.size() && "No inputs to link"); - // TODO: Drop --bc-library in favor of the -l / .a archive path once it is - // established. - // Get all library files. - Expected<SmallVector<std::string>> BCLibFiles = getBCLibraryNames(Args); - if (!BCLibFiles) - return BCLibFiles.takeError(); - // Create a new file to write the linked file to. auto BitcodeOutput = createTempFile(Args, sys::path::filename(OutputFile), "bc"); @@ -343,10 +275,8 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, llvm::map_range(InputBuffers, [](const auto &B) { return B->getBufferIdentifier(); }), ", "); - std::string LibInputs = - llvm::join((*BCLibFiles).begin(), (*BCLibFiles).end(), ", "); - errs() << formatv("link: inputs: {0} libfiles: {1} output: {2}\n", Inputs, - LibInputs, *BitcodeOutput); + errs() << formatv("link: inputs: {0} output: {1}\n", Inputs, + *BitcodeOutput); } // Link input files. Resolve the target triple. @@ -356,9 +286,6 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, Linker L(*LinkerOutput); for (const auto &Buffer : InputBuffers) { - // Data is already in memory; use eager parse (unlike getBitcodeModule which - // stays lazy for --bc-library files where LinkOnlyNeeded skips most - // bodies). auto ModOrErr = parseBitcodeFile(Buffer->getMemBufferRef(), C); if (!ModOrErr) return ModOrErr.takeError(); @@ -386,18 +313,6 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, inconvertibleErrorCode(), "Target triple must be specified or inferable from inputs"); - // Link in library files. - for (auto &File : *BCLibFiles) { - auto LibMod = getBitcodeModule(File, C); - if (!LibMod) - return LibMod.takeError(); - if ((*LibMod)->getTargetTriple() == TargetTriple) { - unsigned Flags = Linker::Flags::LinkOnlyNeeded; - if (L.linkInModule(std::move(*LibMod), Flags)) - return createStringError(inconvertibleErrorCode(), "Could not link IR"); - } - } - // Dump linked output for testing. if (Args.hasArg(OPT_print_linked_module)) outs() << *LinkerOutput; diff --git a/clang/tools/clang-sycl-linker/SYCLLinkOpts.td b/clang/tools/clang-sycl-linker/SYCLLinkOpts.td index 9d1a3347ac0fd..40f758cc7d837 100644 --- a/clang/tools/clang-sycl-linker/SYCLLinkOpts.td +++ b/clang/tools/clang-sycl-linker/SYCLLinkOpts.td @@ -40,13 +40,6 @@ def u : JoinedOrSeparate<["-"], "u">, MetaVarName<"<symbol>">, HelpText<"Force undefined symbol during linking">; def undefined : JoinedOrSeparate<["--"], "undefined">, Alias<u>; -def bc_library : Separate<["--", "-"], "bc-library">, MetaVarName<"<name>">, - HelpText<"Add LLVM bitcode library <name> (with extension) to the link. A " - "relative <name> is resolved against -L paths; an absolute path is " - "taken as-is.">; -def bc_library_EQ : Joined<["--", "-"], "bc-library=">, Flags<[HelpHidden]>, - Alias<bc_library>; - def arch_EQ : Joined<["--", "-"], "arch=">, Flags<[LinkerOnlyOption]>, MetaVarName<"<arch>">, >From ca91547ba9d8413c20451968f223ae223e07dc2a Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 4 Jun 2026 11:26:12 -0700 Subject: [PATCH 3/9] [clang][sycl-linker] Remove inconvertibleErrorCode() argument from createStringError calls Remove the optional inconvertibleErrorCode() argument from all createStringError calls in clang-sycl-linker, simplifying error creation code. Co-Authored-By: Claude Sonnet 4.5 <[email protected]> --- clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 98ef9c09cb135..2d55ca3828969 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -212,8 +212,7 @@ getInput(const ArgList &Args) { } if (InputDescs.empty()) - return createStringError(inconvertibleErrorCode(), - "No input files provided"); + return createStringError("No input files provided"); // Gather search paths and forced undefined symbols SmallVector<StringRef> LibraryPaths; @@ -238,8 +237,7 @@ getInput(const ArgList &Args) { return ResolvedOrErr.takeError(); if (ResolvedOrErr->Buffers.empty()) - return createStringError(inconvertibleErrorCode(), - "No input files could be resolved"); + return createStringError("No input files could be resolved"); return std::move(ResolvedOrErr->Buffers); } @@ -297,7 +295,6 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, TripleSource = Buffer->getBufferIdentifier(); } else { return createStringError( - inconvertibleErrorCode(), "conflicting target triples: '" + TargetTriple.str() + "' (from " + TripleSource + ") vs '" + T.str() + "' (from " + Buffer->getBufferIdentifier() + ")"); @@ -305,12 +302,11 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, } if (L.linkInModule(std::move(*ModOrErr))) - return createStringError(inconvertibleErrorCode(), "Could not link IR"); + return createStringError("Could not link IR"); } if (TargetTriple.empty()) return createStringError( - inconvertibleErrorCode(), "Target triple must be specified or inferable from inputs"); // Dump linked output for testing. >From e8ee597009f7318fc9e37a67837d94ac1bd11ae0 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 4 Jun 2026 11:40:43 -0700 Subject: [PATCH 4/9] [docs][clang-sycl-linker] Update documentation for archive library support Update ClangSYCLLinker.rst to document the new archive library linking functionality added in previous commits: - Replace --bc-library option documentation with -l, --whole-archive, --no-whole-archive, and -u options - Add comprehensive "Library Linking" section explaining: - How to package bitcode files into .a archives with llvm-ar - Standard library search semantics (-l name, -l :exact-name) - Lazy linking behavior and symbol-driven member extraction - --whole-archive for forcing complete extraction - -u flag for forcing undefined symbols - Add implementation note about shared llvm::offloading::resolveArchiveMembers() - Expand examples section with practical archive linking scenarios Co-Authored-By: Claude Sonnet 4.5 <[email protected]> --- clang/docs/ClangSYCLLinker.rst | 80 ++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/clang/docs/ClangSYCLLinker.rst b/clang/docs/ClangSYCLLinker.rst index c28c9fefaace3..7cf5c05eb5a0a 100644 --- a/clang/docs/ClangSYCLLinker.rst +++ b/clang/docs/ClangSYCLLinker.rst @@ -50,7 +50,10 @@ be passed down to downstream AOT compilation tools like 'ocloc' and 'opencl-aot' -help-hidden Display all available options -help Display available options (--help-hidden for more) -L <dir> Add <dir> to the library search path - --bc-library <name> Add LLVM bitcode library <name> (with extension) to the link. A relative <name> is resolved against -L paths; an absolute path is taken as-is. + -l <libname> Search for library <libname> + --whole-archive Include all archive members in the link + --no-whole-archive Only include archive members that resolve undefined symbols (default) + -u <symbol> Force undefined symbol during linking --module-split-mode=<mode> Module split mode: 'source' (default), 'kernel', or 'none' --ocloc-options=<value> Options passed to ocloc for Intel GPU AOT compilation --opencl-aot-options=<value> Options passed to opencl-aot for Intel CPU AOT compilation @@ -61,8 +64,58 @@ be passed down to downstream AOT compilation tools like 'ocloc' and 'opencl-aot' -v Print verbose information -spirv-dump-device-code=<dir> Directory to dump SPIR-V IR code into -Example -======= +Library Linking +=============== + +Device bitcode libraries can be packaged into archive libraries (``.a`` files) +using ``llvm-ar`` and linked using the ``-l`` option: + +.. code-block:: console + + llvm-ar rc libdevice.a func1.bc func2.bc func3.bc + clang-sycl-linker input.bc -l device -L /path/to/libs + +The linker supports standard archive library search semantics: + +* ``-l <name>`` searches for ``lib<name>.a`` in the directories specified by ``-L`` +* ``-l :<exact-name>`` searches for the exact filename in the ``-L`` paths +* Absolute paths can be passed as positional arguments: ``clang-sycl-linker input.bc /path/to/libdevice.a`` + +By default, archive linking is **lazy** - only archive members (individual ``.bc`` files) +that resolve undefined symbols are extracted and linked. This happens at file +granularity: if any symbol in a ``.bc`` file is needed, all symbols in that file +are included. The linker uses a symbol-driven fixed-point algorithm: it +repeatedly scans archives to extract members that resolve currently undefined +symbols until no more extractions occur. + +To force extraction of all archive members regardless of symbol resolution, use +``--whole-archive``: + +.. code-block:: console + + clang-sycl-linker input.bc --whole-archive -l device --no-whole-archive -l other + +The ``-u <symbol>`` option can be used to force a symbol to be undefined, which +can trigger extraction of archive members that define that symbol: + +.. code-block:: console + + clang-sycl-linker input.bc -u my_init_function -l device + +Implementation +-------------- + +Archive linking in ``clang-sycl-linker`` is implemented using the shared +``llvm::offloading::resolveArchiveMembers()`` API from +``llvm/lib/Frontend/Offloading/ArchiveLinker.cpp``. This same infrastructure is +also used by ``clang-nvlink-wrapper``, ensuring consistent archive linking +semantics across offloading tools. + +Examples +======== + +Basic Usage +----------- This tool is intended to be invoked when targeting any of the target offloading toolchains. When the --sycl-link option is passed to the clang driver, the @@ -74,3 +127,24 @@ generate the final executable. .. code-block:: console clang-sycl-linker --triple spirv64 --arch bmg_g21 input.bc + +Linking with Device Libraries +------------------------------ + +To link device bitcode libraries, first package them into archive files: + +.. code-block:: console + + # Create device library archives + llvm-ar rc libmath.a sin.bc cos.bc tan.bc + llvm-ar rc libutils.a helper1.bc helper2.bc + + # Link with lazy loading (only needed members extracted) + clang-sycl-linker --triple spirv64 kernel.bc -l math -l utils -L /path/to/libs -o kernel.spv + + # Force all members to be included from libmath.a + clang-sycl-linker --triple spirv64 kernel.bc --whole-archive -l math --no-whole-archive -l utils -L /path/to/libs -o kernel.spv + + # Use exact archive filename or absolute path + clang-sycl-linker --triple spirv64 kernel.bc -l :libmath.a -L /path/to/libs -o kernel.spv + clang-sycl-linker --triple spirv64 kernel.bc /absolute/path/libmath.a -o kernel.spv >From f01aab172bb5290fb8b0920e2f15e6d2f93e68bf Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 4 Jun 2026 11:44:15 -0700 Subject: [PATCH 5/9] [Frontend][Offloading] Convert InputDesc::KindTy to scoped enum Change InputDesc::KindTy from an unscoped enum to an enum class to improve type safety and avoid namespace pollution. Update all usages in ArchiveLinker.cpp, ClangSYCLLinker.cpp, and ClangNVLinkWrapper.cpp to use the qualified enum names (InputDesc::KindTy::Library and InputDesc::KindTy::File). Co-Authored-By: Claude Sonnet 4.5 <[email protected]> --- clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp | 4 ++-- clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp | 4 ++-- llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h | 2 +- llvm/lib/Frontend/Offloading/ArchiveLinker.cpp | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp b/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp index c4db56b150d28..25ba9dd4a82c1 100644 --- a/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp +++ b/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp @@ -345,8 +345,8 @@ Expected<SmallVector<StringRef>> getInput(const ArgList &Args) { offloading::InputDesc Desc; Desc.Value = Arg->getValue(); Desc.Kind = Arg->getOption().matches(OPT_library) - ? offloading::InputDesc::Library - : offloading::InputDesc::File; + ? offloading::InputDesc::KindTy::Library + : offloading::InputDesc::KindTy::File; Desc.WholeArchive = WholeArchive; InputDescs.push_back(Desc); } diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 2d55ca3828969..2213fe03f079d 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -205,8 +205,8 @@ getInput(const ArgList &Args) { offloading::InputDesc Desc; Desc.Value = Arg->getValue(); Desc.Kind = Arg->getOption().matches(OPT_library) - ? offloading::InputDesc::Library - : offloading::InputDesc::File; + ? offloading::InputDesc::KindTy::Library + : offloading::InputDesc::KindTy::File; Desc.WholeArchive = WholeArchive; InputDescs.push_back(Desc); } diff --git a/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h b/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h index 8594b0bfb2ba0..4dd7280860eaf 100644 --- a/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h +++ b/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h @@ -72,7 +72,7 @@ struct Symbol { /// Description of a single input (file or library). struct InputDesc { StringRef Value; // file path, or library name for -l (the value after -l) - enum KindTy { File, Library } Kind; + enum class KindTy { File, Library } Kind; bool WholeArchive; // --whole-archive state in effect at this input }; diff --git a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp index e861fbe0a9927..a39235c1a942c 100644 --- a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp +++ b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp @@ -180,7 +180,7 @@ resolveArchiveMembers(const Inputs &In, for (const InputDesc &Desc : In.Order) { std::optional<std::string> Filename; - if (Desc.Kind == InputDesc::Library) { + if (Desc.Kind == InputDesc::KindTy::Library) { Filename = searchLibrary(Desc.Value, In.Root, In.SearchPaths); if (!Filename) return createStringError("unable to find library -l%s", >From c85ed180cdffc4861afbfa6ae86c96d6a03b0ce7 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 4 Jun 2026 11:55:31 -0700 Subject: [PATCH 6/9] [clang-sycl-linker] Improve diagnostics for invalid input files Address feedback from https://github.com/llvm/llvm-project/pull/201253#pullrequestreview-4423667514 1. Add file type validation in linkInputs() before attempting bitcode parse. resolveArchiveMembers can return ELF relocatable objects extracted from archives; without this check they produce low-level bitcode parse errors instead of clear "Unsupported file type" diagnostics. 2. Restore validation for positional file inputs in getInput(). resolveArchiveMembers silently skips non-existent paths, so typoed or missing files can be ignored and degrade to the generic "No input files could be resolved" error. Validate existence upfront to report "input file not found: '<path>'" instead. 3. Restore regression test for non-existent positional inputs that was removed in 9224b12691b1. Enhance FILETYPEERROR test to verify filename appears in diagnostic. Also improve ArchiveLinker error message to include filename when rejecting unsupported file types at the resolveArchiveMembers level. Co-Authored-By: Claude Sonnet 4.5 <[email protected]> --- .../test/OffloadTools/clang-sycl-linker/basic.ll | 6 +++++- .../tools/clang-sycl-linker/ClangSYCLLinker.cpp | 16 ++++++++++++++++ llvm/lib/Frontend/Offloading/ArchiveLinker.cpp | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/clang/test/OffloadTools/clang-sycl-linker/basic.ll b/clang/test/OffloadTools/clang-sycl-linker/basic.ll index 44d344657de3c..9c3a8eb508a52 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/basic.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/basic.ll @@ -20,6 +20,10 @@ ; RUN: not clang-sycl-linker -o %t.out 2>&1 | FileCheck %s --check-prefix=NO-INPUT ; NO-INPUT: No input files provided ; +; Test non-existent input file +; RUN: not clang-sycl-linker %t-missing.bc -o %t.out 2>&1 | FileCheck %s --check-prefix=MISSING +; MISSING: input file not found: '{{.*}}-missing.bc' +; ; Test the dry run of a simple case to link two input files. ; Test that IMG_SPIRV image kind is set for non-AOT compilation. ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc -o %t/spirv.out 2>&1 \ @@ -69,7 +73,7 @@ ; RUN: touch %t/dummy.o ; RUN: not clang-sycl-linker %t/dummy.o -o a.spv 2>&1 \ ; RUN: | FileCheck %s --check-prefix=FILETYPEERROR -; FILETYPEERROR: Unsupported file type +; FILETYPEERROR: Unsupported file type: '{{.*}}dummy.o' ; ; Test to see if device library related errors are emitted. ; RUN: not clang-sycl-linker --dry-run %t/input1.bc %t/input2.bc --library-path=%t/libs -l device -l nonexistent -o a.spv 2>&1 \ diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 2213fe03f079d..f03e56edc34e0 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -208,6 +208,16 @@ getInput(const ArgList &Args) { ? offloading::InputDesc::KindTy::Library : offloading::InputDesc::KindTy::File; Desc.WholeArchive = WholeArchive; + + // Validate positional file inputs exist before passing to resolveArchiveMembers + // (which silently skips non-existent paths) + if (Desc.Kind == offloading::InputDesc::KindTy::File) { + if (!sys::fs::exists(Desc.Value)) + return createStringError("input file not found: '" + Desc.Value + "'"); + if (sys::fs::is_directory(Desc.Value)) + return createStringError("'" + Desc.Value + "': Is a directory"); + } + InputDescs.push_back(Desc); } @@ -284,6 +294,12 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, Linker L(*LinkerOutput); for (const auto &Buffer : InputBuffers) { + // Check file type before attempting to parse as bitcode + file_magic Magic = identify_magic(Buffer->getBuffer()); + if (Magic != file_magic::bitcode) + return createStringError("Unsupported file type: '" + + Buffer->getBufferIdentifier() + "'"); + auto ModOrErr = parseBitcodeFile(Buffer->getMemBufferRef(), C); if (!ModOrErr) return ModOrErr.takeError(); diff --git a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp index a39235c1a942c..cb99e84a6e7e0 100644 --- a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp +++ b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp @@ -226,7 +226,7 @@ resolveArchiveMembers(const Inputs &In, break; } default: - return createStringError("Unsupported file type"); + return createStringError("Unsupported file type: '" + *Filename + "'"); } } >From 5fe0f0e459bcca73a64507eec5d65e3d80921286 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 4 Jun 2026 13:25:00 -0700 Subject: [PATCH 7/9] [Frontend][Offloading] Rename InputDesc::KindTy to InputDesc::Kind Remove redundant 'Ty' suffix from InputDesc enum to comply with LLVM Coding Standards. Enum discriminators should use 'Kind' suffix, not 'KindTy'. Rename the InputDesc::Kind member to InputDesc::InputKind to avoid shadowing the enum type name. No functional changes. --- clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp | 6 +++--- clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp | 8 ++++---- llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h | 4 +++- llvm/lib/Frontend/Offloading/ArchiveLinker.cpp | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp b/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp index 25ba9dd4a82c1..ebe12ede61b53 100644 --- a/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp +++ b/clang/tools/clang-nvlink-wrapper/ClangNVLinkWrapper.cpp @@ -344,9 +344,9 @@ Expected<SmallVector<StringRef>> getInput(const ArgList &Args) { offloading::InputDesc Desc; Desc.Value = Arg->getValue(); - Desc.Kind = Arg->getOption().matches(OPT_library) - ? offloading::InputDesc::KindTy::Library - : offloading::InputDesc::KindTy::File; + Desc.InputKind = Arg->getOption().matches(OPT_library) + ? offloading::InputDesc::Kind::Library + : offloading::InputDesc::Kind::File; Desc.WholeArchive = WholeArchive; InputDescs.push_back(Desc); } diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index f03e56edc34e0..ee3c3cf91329c 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -204,14 +204,14 @@ getInput(const ArgList &Args) { offloading::InputDesc Desc; Desc.Value = Arg->getValue(); - Desc.Kind = Arg->getOption().matches(OPT_library) - ? offloading::InputDesc::KindTy::Library - : offloading::InputDesc::KindTy::File; + Desc.InputKind = Arg->getOption().matches(OPT_library) + ? offloading::InputDesc::Kind::Library + : offloading::InputDesc::Kind::File; Desc.WholeArchive = WholeArchive; // Validate positional file inputs exist before passing to resolveArchiveMembers // (which silently skips non-existent paths) - if (Desc.Kind == offloading::InputDesc::KindTy::File) { + if (Desc.InputKind == offloading::InputDesc::Kind::File) { if (!sys::fs::exists(Desc.Value)) return createStringError("input file not found: '" + Desc.Value + "'"); if (sys::fs::is_directory(Desc.Value)) diff --git a/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h b/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h index 4dd7280860eaf..5efa728b5796d 100644 --- a/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h +++ b/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h @@ -71,8 +71,10 @@ struct Symbol { /// Description of a single input (file or library). struct InputDesc { + enum class Kind { File, Library }; + StringRef Value; // file path, or library name for -l (the value after -l) - enum class KindTy { File, Library } Kind; + Kind InputKind; bool WholeArchive; // --whole-archive state in effect at this input }; diff --git a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp index cb99e84a6e7e0..b58168bee6e51 100644 --- a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp +++ b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp @@ -180,7 +180,7 @@ resolveArchiveMembers(const Inputs &In, for (const InputDesc &Desc : In.Order) { std::optional<std::string> Filename; - if (Desc.Kind == InputDesc::KindTy::Library) { + if (Desc.InputKind == InputDesc::Kind::Library) { Filename = searchLibrary(Desc.Value, In.Root, In.SearchPaths); if (!Filename) return createStringError("unable to find library -l%s", >From c17f0e7b4808ab5f137fcf1274c57e113171a6ac Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 4 Jun 2026 13:28:23 -0700 Subject: [PATCH 8/9] [Frontend][Offloading] Simplify ArchiveLinker file headers Remove filename and description from file header comments to follow LLVM coding standards consistently across the Frontend/Offloading directory. Co-Authored-By: Claude Sonnet 4.5 <[email protected]> --- llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h | 2 +- llvm/lib/Frontend/Offloading/ArchiveLinker.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h b/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h index 5efa728b5796d..fde28f5a04600 100644 --- a/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h +++ b/llvm/include/llvm/Frontend/Offloading/ArchiveLinker.h @@ -1,4 +1,4 @@ -//===- ArchiveLinker.h - Archive member selection for offloading -*- C++ -*-=// +//===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. diff --git a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp index b58168bee6e51..4e8514f668d8f 100644 --- a/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp +++ b/llvm/lib/Frontend/Offloading/ArchiveLinker.cpp @@ -1,4 +1,4 @@ -//===- ArchiveLinker.cpp - Archive member selection for offloading --------===// +//===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. >From 859034755f2102cd86d55f39e83288dd947ab773 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 4 Jun 2026 14:54:36 -0700 Subject: [PATCH 9/9] clang-format --- clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index ee3c3cf91329c..e056a28ad0d0c 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -209,8 +209,8 @@ getInput(const ArgList &Args) { : offloading::InputDesc::Kind::File; Desc.WholeArchive = WholeArchive; - // Validate positional file inputs exist before passing to resolveArchiveMembers - // (which silently skips non-existent paths) + // Validate positional file inputs exist before passing to + // resolveArchiveMembers (which silently skips non-existent paths) if (Desc.InputKind == offloading::InputDesc::Kind::File) { if (!sys::fs::exists(Desc.Value)) return createStringError("input file not found: '" + Desc.Value + "'"); @@ -312,8 +312,8 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> InputBuffers, } else { return createStringError( "conflicting target triples: '" + TargetTriple.str() + "' (from " + - TripleSource + ") vs '" + T.str() + "' (from " + - Buffer->getBufferIdentifier() + ")"); + TripleSource + ") vs '" + T.str() + "' (from " + + Buffer->getBufferIdentifier() + ")"); } } _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
