-
Notifications
You must be signed in to change notification settings - Fork 106
[HIPIFY][feature] Local header hipification via Clang PPCallbacks (experimental, opt-in) - Part 1 #2402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: amd-develop
Are you sure you want to change the base?
[HIPIFY][feature] Local header hipification via Clang PPCallbacks (experimental, opt-in) - Part 1 #2402
Changes from all commits
b10955d
954d8b9
d58ca02
cfa1f9f
0c45a34
c7b56b7
dce5aaf
7aeaf1d
3e3db8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,167 +1,280 @@ | ||
| /* | ||
| Copyright (c) 2026 - present Advanced Micro Devices, Inc. All rights reserved. | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in | ||
| all copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| THE SOFTWARE. | ||
| */ | ||
|
|
||
| #include "LocalHeader.h" | ||
| #include "LLVMCompat.h" | ||
|
|
||
| #include <sstream> | ||
| #include <regex> | ||
| #include <memory> | ||
| #include <set> | ||
| #include <vector> | ||
| #include <system_error> | ||
|
|
||
| #include "llvm/Support/FileSystem.h" | ||
| #include "clang/Frontend/CompilerInstance.h" | ||
| #include "clang/Frontend/FrontendActions.h" | ||
| #include "clang/Lex/PPCallbacks.h" | ||
| #include "clang/Lex/Preprocessor.h" | ||
| #include "clang/Tooling/CompilationDatabase.h" | ||
| #include "clang/Tooling/Tooling.h" | ||
| #include "llvm/Support/Path.h" | ||
| #include "llvm/Support/MemoryBuffer.h" | ||
| #include "llvm/Support/raw_ostream.h" | ||
|
|
||
| #include "LLVMCompat.h" | ||
|
|
||
| using namespace clang; | ||
| using namespace clang::tooling; | ||
| using namespace llvm; | ||
| using namespace std; | ||
|
|
||
| static std::string normalizeSmallStringPath(SmallString<256> &p) { | ||
| llvm::sys::path::remove_dots(p, true); | ||
| namespace { | ||
|
|
||
| class IncludeCollectorCallbacks : public clang::PPCallbacks { | ||
| const clang::SourceManager &SM; | ||
| std::vector<IncludeEntry> &Entries; | ||
|
|
||
| public: | ||
| IncludeCollectorCallbacks(const clang::SourceManager &SM, | ||
| std::vector<IncludeEntry> &entries) | ||
| : SM(SM), Entries(entries) {} | ||
|
|
||
| SmallString<256> realBuf; | ||
| std::error_code ec = llvm::sys::fs::real_path(p, realBuf); | ||
| if (!ec) { | ||
| return std::string(realBuf.str()); | ||
| void InclusionDirective(clang::SourceLocation hash_loc, | ||
| const clang::Token &include_token, | ||
| StringRef file_name, bool is_angled, | ||
| clang::CharSourceRange filename_range, | ||
| #if LLVM_VERSION_MAJOR < 15 | ||
| const clang::FileEntry *file, | ||
| #elif LLVM_VERSION_MAJOR == 15 | ||
| Optional<clang::FileEntryRef> file, | ||
| #else | ||
| clang::OptionalFileEntryRef file, | ||
| #endif | ||
| StringRef search_path, StringRef relative_path, | ||
| #if LLVM_VERSION_MAJOR < 19 | ||
| const clang::Module *SuggestedModule | ||
| #else | ||
| const clang::Module *SuggestedModule, | ||
| bool ModuleImported | ||
| #endif | ||
| #if LLVM_VERSION_MAJOR > 6 | ||
| , | ||
| clang::SrcMgr::CharacteristicKind FileType | ||
| #endif | ||
|
Comment on lines
+55
to
+76
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Several parameters in |
||
| ) override { | ||
| if (!SM.isWrittenInMainFile(hash_loc)) | ||
| return; | ||
|
Comment on lines
+78
to
+79
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
|
|
||
| IncludeEntry entry; | ||
| entry.fileName = file_name.str(); | ||
| entry.isAngled = is_angled; | ||
|
|
||
| if (file) { | ||
| #if LLVM_VERSION_MAJOR < 15 | ||
| entry.resolvedPath = file->tryGetRealPathName().str(); | ||
| if (entry.resolvedPath.empty()) | ||
| entry.resolvedPath = file->getName().str(); | ||
| #else | ||
| entry.resolvedPath = file->getFileEntry().tryGetRealPathName().str(); | ||
| if (entry.resolvedPath.empty()) | ||
| entry.resolvedPath = file->getName().str(); | ||
| #endif | ||
| } | ||
|
|
||
| Entries.push_back(std::move(entry)); | ||
| } | ||
| }; | ||
|
|
||
| return std::string(p.str()); | ||
| } | ||
| class IncludeCollectorAction : public clang::PreprocessorFrontendAction { | ||
| std::vector<IncludeEntry> &Entries; | ||
|
|
||
| static bool pathExists(const std::string &p) { | ||
| SmallString<256> in(p.begin(), p.end()); | ||
| public: | ||
| explicit IncludeCollectorAction(std::vector<IncludeEntry> &entries) | ||
| : Entries(entries) {} | ||
|
|
||
| SmallString<256> realBuf; | ||
| std::error_code ec = llvm::sys::fs::real_path(in, realBuf); | ||
| if (!ec) return true; | ||
| void ExecuteAction() override { | ||
| clang::CompilerInstance &CI = getCompilerInstance(); | ||
| clang::Preprocessor &PP = CI.getPreprocessor(); | ||
| PP.addPPCallbacks(std::make_unique<IncludeCollectorCallbacks>( | ||
|
ranapratap55 marked this conversation as resolved.
|
||
| CI.getSourceManager(), Entries)); | ||
|
|
||
| SmallString<256> norm = in; | ||
| llvm::sys::path::remove_dots(norm, true); | ||
| return llvm::sys::fs::exists(norm); | ||
| } | ||
| PP.EnterMainSourceFile(); | ||
| clang::Token Tok; | ||
| do { | ||
| PP.Lex(Tok); | ||
| } while (Tok.isNot(clang::tok::eof)); | ||
| } | ||
|
Comment on lines
+108
to
+119
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overriding |
||
| }; | ||
|
|
||
| namespace { | ||
| static const std::regex LocalIncludeRe( | ||
| R"(^\s*#\s*include\s*\"([^\"\n]+)\"\s*(?://.*)?$)", std::regex::ECMAScript); | ||
| class IncludeCollectorActionFactory : public FrontendActionFactory { | ||
| std::vector<IncludeEntry> &Entries; | ||
|
|
||
| bool readFile(const std::string &path, std::string &out) { | ||
| auto MBOrErr = llvm::MemoryBuffer::getFile(path); | ||
| if (!MBOrErr) return false; | ||
| out = MBOrErr->get()->getBuffer().str(); | ||
| return true; | ||
| public: | ||
| explicit IncludeCollectorActionFactory(std::vector<IncludeEntry> &entries) | ||
| : Entries(entries) {} | ||
|
|
||
| #if LLVM_VERSION_MAJOR >= 10 | ||
| std::unique_ptr<clang::FrontendAction> create() override { | ||
| return std::make_unique<IncludeCollectorAction>(Entries); | ||
| } | ||
| #else | ||
| clang::FrontendAction *create() override { | ||
| return new IncludeCollectorAction(Entries); | ||
| } | ||
| #endif | ||
| }; | ||
|
|
||
| bool resolveLocalIncludeInternal(const std::string &mainSourceAbsPath, | ||
| const std::string &includeTok, | ||
| std::string &outAbs) { | ||
| SmallString<256> base(mainSourceAbsPath); | ||
| sys::path::remove_filename(base); | ||
| SmallString<256> candidate(base); | ||
| sys::path::append(candidate, includeTok); | ||
| sys::path::remove_dots(candidate, true); | ||
| if (pathExists(std::string(candidate.str()))) { | ||
| outAbs = normalizeSmallStringPath(candidate); | ||
| return true; | ||
| } | ||
| } // namespace | ||
|
|
||
| bool appendArgumentsAdjusters(ct::RefactoringTool &Tool, | ||
| const std::string &sSourceAbsPath, | ||
| const char *hipify_exe); | ||
|
|
||
| bool collectIncludeTree(const std::string &srcPath, | ||
| const ct::CompilationDatabase *compDB, | ||
| ct::CommonOptionsParser *OptionsParserPtr, | ||
| const char *hipify_exe, | ||
| const std::string &mainContextPath, | ||
| std::vector<IncludeEntry> &outEntries) { | ||
| outEntries.clear(); | ||
|
|
||
| const ct::CompilationDatabase &baseDB = | ||
| compDB ? *compDB : OptionsParserPtr->getCompilations(); | ||
|
|
||
| // If srcPath has no entry in the compilation database, fall back to a | ||
| // FixedCompilationDatabase rooted at the mainContextPath's directory so that | ||
| // the tool doesn't skip the file. | ||
| std::vector<ct::CompileCommand> cmds = baseDB.getCompileCommands(srcPath); | ||
| std::unique_ptr<ct::FixedCompilationDatabase> fallbackDB; | ||
| if (cmds.empty()) { | ||
| std::string dir = sys::path::parent_path(mainContextPath).str(); | ||
| fallbackDB = std::make_unique<ct::FixedCompilationDatabase>( | ||
| dir, std::vector<std::string>()); | ||
| } | ||
|
Comment on lines
+162
to
+166
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing an empty vector to |
||
|
|
||
| ct::RefactoringTool Tool(fallbackDB ? *fallbackDB : baseDB, {srcPath}); | ||
|
|
||
| if (!appendArgumentsAdjusters(Tool, mainContextPath, hipify_exe)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| bool resolveLocalInclude(const std::string &mainSourceAbsPath, | ||
| const std::string &includeToken, | ||
| std::string &outAbsPath) { | ||
| return resolveLocalIncludeInternal(mainSourceAbsPath, includeToken, outAbsPath); | ||
| IncludeCollectorActionFactory factory(outEntries); | ||
| Tool.run(&factory); | ||
| return true; | ||
| } | ||
|
|
||
| bool collectLocalQuotedIncludes(const std::string &mainSourceAbsPath, | ||
| const ct::CompilationDatabase *compDB, | ||
| ct::CommonOptionsParser *OptionsParserPtr, | ||
| const char *hipify_exe, | ||
| std::vector<std::string> &outHeaders) { | ||
| std::string content; | ||
| if (!readFile(mainSourceAbsPath, content)) { | ||
| errs() << "\n" << sHipify << sError << "Cannot read source file: " << mainSourceAbsPath << "\n"; | ||
| std::vector<IncludeEntry> entries; | ||
| if (!collectIncludeTree(mainSourceAbsPath, compDB, OptionsParserPtr, | ||
| hipify_exe, mainSourceAbsPath, entries)) { | ||
| errs() << "\n" | ||
| << sHipify << sError | ||
| << "Failed to collect includes from: " << mainSourceAbsPath << "\n"; | ||
| return false; | ||
| } | ||
|
|
||
| std::set<std::string> uniq; | ||
| std::smatch m; | ||
| std::istringstream iss(content); | ||
| std::string line; | ||
| while (std::getline(iss, line)) { | ||
| if (std::regex_match(line, m, LocalIncludeRe)) { | ||
| std::string rel = m[1].str(); | ||
| std::string abs; | ||
| if (resolveLocalIncludeInternal(mainSourceAbsPath, rel, abs)){ | ||
| uniq.insert(abs); | ||
| } else { | ||
| errs() << sHipify << sWarning | ||
| << "Missing local header referenced: \"" << rel | ||
| << "\" in " << mainSourceAbsPath << "\n"; | ||
| } | ||
| } | ||
| for (const auto &e : entries) { | ||
| if (!e.isAngled && !e.resolvedPath.empty()) | ||
| uniq.insert(e.resolvedPath); | ||
| } | ||
| outHeaders.assign(uniq.begin(), uniq.end()); | ||
| return true; | ||
| } | ||
|
|
||
| bool hipifyLocalHeaders(const std::string &mainSourceAbsPath, | ||
| const ct::CompilationDatabase *compDB, | ||
| ct::CommonOptionsParser *OptionsParserPtr, | ||
| const char *hipify_exe, | ||
| bool recursive) { | ||
| const ct::CompilationDatabase *compDB, | ||
| ct::CommonOptionsParser *OptionsParserPtr, | ||
| const char *hipify_exe, bool recursive) { | ||
|
|
||
| std::vector<std::string> initial; | ||
| if (!collectLocalQuotedIncludes(mainSourceAbsPath, initial)) { | ||
| if (!collectLocalQuotedIncludes(mainSourceAbsPath, compDB, OptionsParserPtr, | ||
| hipify_exe, initial)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (initial.empty()) { | ||
| outs() << sHipify << "No local headers detected in " << mainSourceAbsPath << "\n"; | ||
| outs() << "\n" << sHipify << "No local headers detected in " | ||
| << sys::path::filename(mainSourceAbsPath) << "\n"; | ||
| return true; | ||
| } | ||
|
|
||
| outs() << "\n" << sHipify << "Local headers found: " << initial.size() | ||
| << " in " << sys::path::filename(mainSourceAbsPath) << "\n"; | ||
| for (size_t i = 0; i < initial.size(); ++i) { | ||
| outs() << (i + 1) << "/" << initial.size() | ||
| << ": " << sys::path::filename(initial[i]) << "\n"; | ||
| } | ||
|
|
||
| std::vector<std::string> work(initial.begin(), initial.end()); | ||
| std::set<std::string> processed; | ||
| std::set<std::string> queued(initial.begin(), initial.end()); | ||
| size_t total = initial.size(); | ||
| size_t current = 0; | ||
|
|
||
| while (!work.empty()) { | ||
| std::string hdr = work.back(); | ||
| work.pop_back(); | ||
| if (processed.count(hdr)) { | ||
| errs() << sHipify << sWarning << "Duplicate local header reference ignored: " << hdr << "\n"; | ||
| continue; | ||
| } | ||
| processed.insert(hdr); | ||
|
|
||
| std::string original; | ||
| if (!readFile(hdr, original)) { | ||
| errs() << sHipify << sError << "Cannot read header: " << hdr << "\n"; | ||
| continue; | ||
| } | ||
| ++current; | ||
|
|
||
| std::string hipOut = hdr + ".hip"; | ||
| bool ok = hipifySingleSource(hdr, hipOut, compDB, OptionsParserPtr, | ||
| hipify_exe, mainSourceAbsPath, false); | ||
| hipify_exe, mainSourceAbsPath, false); | ||
|
Comment on lines
242
to
+243
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like a regression. The context vector is never passed to Additionally, the current tests yield |
||
|
|
||
| if (!ok) { | ||
| errs() << sHipify << sError << "Hipify failed for header: " << hdr << "\n"; | ||
| errs() << "\n" << sHipify << sError | ||
| << "Hipify failed for header [" << current << "/" << total | ||
| << "]: " << sys::path::filename(hdr) << "\n"; | ||
| return false; | ||
| } | ||
| outs() << sHipify << "Successfully hipified header file" << "\n"; | ||
|
|
||
| if (recursive) { | ||
| std::smatch m; | ||
| std::istringstream iss(original); | ||
| std::string line; | ||
| while (std::getline(iss, line)) { | ||
| if (std::regex_match(line, m, LocalIncludeRe)) { | ||
| std::string rel = m[1].str(); | ||
| std::string abs; | ||
| if (resolveLocalIncludeInternal(hdr, rel, abs) && | ||
| !processed.count(abs)) | ||
| work.push_back(abs); | ||
| std::vector<IncludeEntry> childEntries; | ||
| if (collectIncludeTree(hdr, compDB, OptionsParserPtr, hipify_exe, | ||
| mainSourceAbsPath, childEntries)) { | ||
| std::vector<std::string> newHeaders; | ||
| for (const auto &e : childEntries) { | ||
| if (!e.isAngled && !e.resolvedPath.empty() && | ||
| !processed.count(e.resolvedPath) && | ||
| !queued.count(e.resolvedPath)) { | ||
| newHeaders.push_back(e.resolvedPath); | ||
| work.push_back(e.resolvedPath); | ||
| queued.insert(e.resolvedPath); | ||
| } | ||
| } | ||
| if (!newHeaders.empty()) { | ||
| total += newHeaders.size(); | ||
| outs() << sHipify << " Recursive: found " << newHeaders.size() | ||
| << " additional local header(s) in " | ||
| << sys::path::filename(hdr) << "\n"; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| outs() << "\n" << sHipify << "Local header hipification complete: " | ||
| << processed.size() << " header(s) processed.\n"; | ||
| return true; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might it be simplified?
Did you test it against different LLVM versions?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested by building HIPIFY against LLVM 14, 15, 16, 18 and all compile without any errors. This covers (
FileEntry*, Optional<FileEntryRef>, OptionalFileEntryRef, and with/without ModuleImported).