Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 206 additions & 93 deletions src/LocalHeader.cpp
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 +59 to +76

Copy link
Copy Markdown
Collaborator

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?

@ranapratap55 ranapratap55 Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

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).

Comment on lines +55 to +76

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several parameters in InclusionDirective signature are named but never actually used in the function body.

) override {
if (!SM.isWrittenInMainFile(hash_loc))
return;
Comment on lines +78 to +79

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using !SM.isWrittenInMainFile(hash_loc) inside the preprocessor callback forcibly prevents Clang from natively traversing nested includes and forces the tool to spawn a brand-new compiler instance for every child header, which destroys macro inheritance. The possible solution here is to remove the check.


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>(
Comment thread
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overriding ExecuteAction() to manually call EnterMainSourceFile() and Lex bypasses standard Clang initialization, such as IgnorePragmas(). If a header contains unknown pragmas, your pre-pass might crash.

};

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing an empty vector to FixedCompilationDatabase wipes out all project compiler flags and breaks header resolution for any real-world project with nested include directories.


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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a regression. The context vector is never passed to hipifySingleSource, and the local headers are being hipified in a vacuum, which should lead to a crash.

Additionally, the current tests yield false positives as they are self-contained. A negative test could help here to prove the injection is actually working.


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;
}
Loading