Iteration 3 of the fold optimization loop (docs/research/optimization-loop-2026-07-30.md). mergeTranslationUnitGraphContributions hard-failed with 'conflicting semantic-USR function evidence' when the same function USR recurred across TU contributions with spelling-variant qualified names (libc++ inline-namespace std:: vs std::__1) or an empty body hash (TU-dependent template instantiation). ProjectCallGraph::addFunction — the exhaustive fallback builder this merge must mirror — merges duplicates leniently, so the incremental path could never succeed on real C++ codebases and every scan silently paid the exhaustive fallback. Measured (fold self-scan, 8 TUs): - call-graph phase with --cache-dir: 24.1s median -> 1.7s (93% cut); exhaustive-fallback warnings eliminated - project eval wall median: 160.1s -> 92.6s (n=2, partly noise) Finding fingerprints are exactly preserved (fold 0fec78e5..., yams 247523c5...; churn 0, pairwise Jaccard 1.0, both project evals ok). Full suite 136/137 (sole failure: pre-existing benchmark_evaluation fixture-schema issue, unrelated area). Red test added: 'contribution merge tolerates same-USR spelling variants like the exhaustive builder' (test_project_index.cpp).
618 lines
30 KiB
C++
618 lines
30 KiB
C++
#include <sqlite3.h>
|
|
|
|
#include <algorithm>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "fold/cli/source_call_graph.hpp"
|
|
#include "fold/index/store.hpp"
|
|
|
|
namespace {
|
|
|
|
fold::index::TranslationUnitGraphContribution contribution(std::string source,
|
|
std::string command_hash = "command-v1",
|
|
std::string header_hash = "header-v1") {
|
|
return {
|
|
.fingerprint = {.source_path = source,
|
|
.compile_command_hash = command_hash,
|
|
.source_hash = "source-v1",
|
|
.toolchain_hash = "clang-v1",
|
|
.dependencies = {{source, "source-v1", fold::index::DependencyKind::Source},
|
|
{"shared.hpp", header_hash,
|
|
fold::index::DependencyKind::Header}}},
|
|
.functions = {{"usr:entry:" + source, "entry", source, "entry-v1", true, false, 1, {}, {}},
|
|
{"usr:shared", "shared", "shared.hpp", "body-v1", true, false, 0, {}, {}}},
|
|
.edges = {{"usr:entry:" + source, "usr:shared", fold::index::EdgeKind::Conservative, 0.75}},
|
|
.unresolved_calls = {{"callsite:" + source,
|
|
"usr:entry:" + source,
|
|
fold::index::UnresolvedCallKind::Virtual,
|
|
"void(Base::*)()",
|
|
"usr:base",
|
|
{"family:a", "family:b"}}},
|
|
.guarded_calls = {{"guarded:" + source, "usr:entry:" + source, "usr:shared",
|
|
fold::index::EdgeKind::Direct,
|
|
fold::ir::ConfigGuard::projected(
|
|
fold::ir::ConfigGuardExpr::boolSymbol(std::string(64, 'a')))}},
|
|
.production_roots = {{"usr:entry:" + source, "entry", source,
|
|
fold::analysis::ProductionRootKind::Main, source, 1, 1, true, false}},
|
|
};
|
|
}
|
|
|
|
std::filesystem::path temporaryDatabase() {
|
|
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
return std::filesystem::temp_directory_path() /
|
|
("fold-project-index-" + std::to_string(stamp) + ".sqlite");
|
|
}
|
|
|
|
void executeSql(const std::filesystem::path& path, const char* sql) {
|
|
sqlite3* database = nullptr;
|
|
REQUIRE(sqlite3_open(path.string().c_str(), &database) == SQLITE_OK);
|
|
char* error = nullptr;
|
|
const int status = sqlite3_exec(database, sql, nullptr, nullptr, &error);
|
|
INFO(std::string(error == nullptr ? "" : error));
|
|
CHECK(status == SQLITE_OK);
|
|
sqlite3_free(error);
|
|
sqlite3_close(database);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("project index round-trips versioned TU contributions",
|
|
"[project-index][tu-contribution]") {
|
|
fold::index::ProjectIndex index(":memory:");
|
|
REQUIRE(index.isOpen());
|
|
const auto expected = contribution("a.cpp");
|
|
REQUIRE(index.replaceTranslationUnitContribution(expected));
|
|
REQUIRE(index.replaceTranslationUnitContribution(expected));
|
|
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok);
|
|
REQUIRE(loaded.contributions.size() == 1);
|
|
CHECK(loaded.contributions.front() == expected);
|
|
}
|
|
|
|
TEST_CASE("project index TU replacement tracks source header and flag changes atomically",
|
|
"[project-index][tu-contribution][replace]") {
|
|
fold::index::ProjectIndex index(":memory:");
|
|
REQUIRE(index.isOpen());
|
|
REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp")));
|
|
|
|
auto changed = contribution("a.cpp", "command-with-DENABLE", "header-v2");
|
|
changed.fingerprint.source_hash = "source-v2";
|
|
changed.functions.erase(changed.functions.begin());
|
|
changed.edges.clear();
|
|
changed.unresolved_calls.clear();
|
|
changed.production_roots.clear();
|
|
REQUIRE(index.replaceTranslationUnitContribution(changed));
|
|
auto loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.contributions.size() == 1);
|
|
CHECK(loaded.contributions.front() == changed);
|
|
|
|
auto invalid = changed;
|
|
invalid.functions.push_back(invalid.functions.front());
|
|
CHECK_FALSE(index.replaceTranslationUnitContribution(invalid));
|
|
loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.contributions.size() == 1);
|
|
CHECK(loaded.contributions.front() == changed);
|
|
}
|
|
|
|
TEST_CASE("project index preserves shared header ownership across TU removal",
|
|
"[project-index][tu-contribution][shared-header]") {
|
|
fold::index::ProjectIndex index(":memory:");
|
|
REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp")));
|
|
auto second = contribution("b.cpp", "command-v2", "header-v2");
|
|
const auto shared =
|
|
std::find_if(second.functions.begin(), second.functions.end(),
|
|
[](const auto& function) { return function.usr == "usr:shared"; });
|
|
REQUIRE(shared != second.functions.end());
|
|
shared->body_hash = "body-v2";
|
|
REQUIRE(index.replaceTranslationUnitContribution(second));
|
|
auto loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.contributions.size() == 2);
|
|
for (const auto& item : loaded.contributions) {
|
|
CHECK(std::any_of(item.functions.begin(), item.functions.end(),
|
|
[](const auto& function) { return function.usr == "usr:shared"; }));
|
|
}
|
|
|
|
REQUIRE(index.removeTranslationUnitContribution("a.cpp"));
|
|
loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.contributions.size() == 1);
|
|
CHECK(loaded.contributions.front().fingerprint.source_path == "b.cpp");
|
|
const auto remaining_shared =
|
|
std::find_if(loaded.contributions.front().functions.begin(),
|
|
loaded.contributions.front().functions.end(),
|
|
[](const auto& function) { return function.usr == "usr:shared"; });
|
|
REQUIRE(remaining_shared != loaded.contributions.front().functions.end());
|
|
CHECK(remaining_shared->body_hash == "body-v2");
|
|
}
|
|
|
|
TEST_CASE("project index self-heals missing contribution schema metadata",
|
|
"[project-index][tu-contribution][schema]") {
|
|
const auto path = temporaryDatabase();
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp")));
|
|
}
|
|
executeSql(path, "DELETE FROM graph_contribution_meta WHERE key='schema_version';");
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.isOpen());
|
|
// Stale graph tables are dropped and recreated at the current schema,
|
|
// so the index reopens writable instead of failing closed forever.
|
|
CHECK(index.replaceTranslationUnitContribution(contribution("b.cpp")));
|
|
CHECK(index.loadTranslationUnitContributions().status ==
|
|
fold::index::ContributionLoadStatus::Ok);
|
|
}
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("project index self-heals future contribution schemas",
|
|
"[project-index][tu-contribution][schema]") {
|
|
const auto path = temporaryDatabase();
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.isOpen());
|
|
REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp")));
|
|
}
|
|
executeSql(path, "UPDATE graph_contribution_meta SET value='999' "
|
|
"WHERE key='schema_version';");
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.isOpen());
|
|
// A future schema invalidates every stored contribution; the cache
|
|
// resets itself to the current schema instead of refusing all writes.
|
|
CHECK(index.replaceTranslationUnitContribution(contribution("b.cpp")));
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
CHECK(loaded.status == fold::index::ContributionLoadStatus::Ok);
|
|
CHECK_FALSE(loaded.contributions.empty());
|
|
}
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("project index rejects malformed contribution rows without partial loads",
|
|
"[project-index][tu-contribution][corrupt]") {
|
|
const auto path = temporaryDatabase();
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp")));
|
|
}
|
|
executeSql(path, "UPDATE graph_edges SET kind=99;");
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
CHECK(loaded.status == fold::index::ContributionLoadStatus::Corrupt);
|
|
CHECK(loaded.contributions.empty());
|
|
}
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("project index rejects malformed persisted production roots",
|
|
"[project-index][tu-contribution][roots][corrupt]") {
|
|
const auto path = temporaryDatabase();
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp")));
|
|
}
|
|
executeSql(path, "UPDATE graph_production_roots SET production=2;");
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
CHECK(loaded.status == fold::index::ContributionLoadStatus::Corrupt);
|
|
CHECK(loaded.contributions.empty());
|
|
}
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("project index rejects malformed persisted configuration guards",
|
|
"[project-index][tu-contribution][guard][corrupt]") {
|
|
const auto path = temporaryDatabase();
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp")));
|
|
}
|
|
executeSql(path, "UPDATE graph_guarded_calls SET guard_json='{\"status\":\"projected\"}';");
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
CHECK(loaded.status == fold::index::ContributionLoadStatus::Corrupt);
|
|
CHECK(loaded.contributions.empty());
|
|
}
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("project index persists USR nodes and typed uncertain edges", "[project-index]") {
|
|
const auto path = temporaryDatabase();
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.isOpen());
|
|
const auto stats = index.replaceTranslationUnit(
|
|
"a.cpp",
|
|
{{"usr:a", "a", "a.cpp", "hash-a", true, false, {}, {}, {}},
|
|
{"usr:b", "b", "a.cpp", "hash-b", true, true, 7, std::string{},
|
|
std::vector<std::string>{}}},
|
|
{{"usr:a", "usr:b", fold::index::EdgeKind::Direct, 1.0},
|
|
{"usr:b", "usr:external", fold::index::EdgeKind::Unknown, 0.0}});
|
|
CHECK(stats.changed_functions == 2);
|
|
CHECK(stats.reused_functions == 0);
|
|
}
|
|
{
|
|
fold::index::ProjectIndex reopened(path.string());
|
|
REQUIRE(reopened.isOpen());
|
|
REQUIRE(reopened.functions().size() == 2);
|
|
const auto edges = reopened.outgoingEdges("usr:b");
|
|
REQUIRE(edges.size() == 1);
|
|
CHECK(edges[0].kind == fold::index::EdgeKind::Unknown);
|
|
CHECK(edges[0].confidence == 0.0);
|
|
CHECK(reopened.functions()[1].escapes);
|
|
CHECK(reopened.functions()[1].scc_id == 7);
|
|
}
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("project index round-trips unified graphs and reports warm reuse",
|
|
"[project-index][graph]") {
|
|
fold::index::ProjectIndex index(":memory:");
|
|
REQUIRE(index.isOpen());
|
|
|
|
fold::ir::ProjectCallGraph cold;
|
|
cold.addFunction("usr:caller", "caller", "caller.cpp", true, "body-caller");
|
|
cold.addFunction("usr:callee", "callee", "callee.cpp", true, "body-callee", true);
|
|
cold.addCallEdge("usr:caller", "usr:callee", fold::ir::CallGraphEdgeKind::Conservative, 0.5);
|
|
cold.computeSCCs();
|
|
const std::vector<std::pair<std::string, std::string>> inputs = {{"file:caller.cpp", "hash-1"},
|
|
{"file:callee.cpp", "hash-2"}};
|
|
const auto first = index.replaceProjectGraph(cold, inputs);
|
|
CHECK(first.changed_functions == 2);
|
|
CHECK(first.reused_functions == 0);
|
|
|
|
fold::ir::ProjectCallGraph loaded;
|
|
REQUIRE(index.loadProjectGraph(loaded));
|
|
REQUIRE(index.matchesInputFingerprints(inputs));
|
|
CHECK(loaded.nodeCount() == 2);
|
|
CHECK(loaded.edgeCount(fold::ir::CallGraphEdgeKind::Conservative) == 1);
|
|
CHECK(loaded.getNode("usr:callee")->escapes);
|
|
|
|
fold::ir::ProjectCallGraph reordered;
|
|
reordered.addFunction("usr:callee", "callee", "callee.cpp", true, "body-callee", true);
|
|
reordered.addFunction("usr:caller", "caller", "caller.cpp", true, "body-caller");
|
|
reordered.addCallEdge("usr:caller", "usr:callee", fold::ir::CallGraphEdgeKind::Conservative,
|
|
0.5);
|
|
reordered.computeSCCs();
|
|
const auto warm = index.replaceProjectGraph(reordered, inputs);
|
|
CHECK(warm.reused_functions == 2);
|
|
CHECK(warm.changed_functions == 0);
|
|
CHECK(warm.changed_usrs.empty());
|
|
|
|
fold::ir::ProjectCallGraph changed;
|
|
changed.addFunction("usr:caller", "caller", "caller.cpp", true, "body-caller");
|
|
changed.addFunction("usr:callee", "callee", "callee.cpp", true, "body-callee-v2", true);
|
|
changed.addCallEdge("usr:caller", "usr:callee", fold::ir::CallGraphEdgeKind::Conservative, 0.5);
|
|
changed.computeSCCs();
|
|
const auto update = index.replaceProjectGraph(changed);
|
|
CHECK(update.reused_functions == 1);
|
|
CHECK(update.changed_functions == 1);
|
|
CHECK(update.invalidated_dependents == 1);
|
|
CHECK(update.changed_usrs == std::vector<std::string>{"usr:callee"});
|
|
CHECK(update.invalidated_usrs == std::vector<std::string>{"usr:caller"});
|
|
|
|
fold::ir::ProjectCallGraph removed;
|
|
removed.addFunction("usr:caller", "caller", "caller.cpp", true, "body-caller");
|
|
removed.computeSCCs();
|
|
const auto removal = index.replaceProjectGraph(removed);
|
|
CHECK(removal.removed_functions == 1);
|
|
CHECK(index.functions().size() == 1);
|
|
}
|
|
|
|
TEST_CASE("project index corruption fails closed", "[project-index][corrupt]") {
|
|
const auto path = temporaryDatabase();
|
|
{
|
|
std::ofstream output(path, std::ios::binary);
|
|
output << "not a sqlite database";
|
|
}
|
|
fold::index::ProjectIndex index(path.string());
|
|
CHECK_FALSE(index.isOpen());
|
|
CHECK_FALSE(index.error().empty());
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("project index replacement reports reuse and invalidates reverse dependents",
|
|
"[project-index][incremental]") {
|
|
fold::index::ProjectIndex index(":memory:");
|
|
REQUIRE(index.isOpen());
|
|
index.replaceTranslationUnit("a.cpp",
|
|
{{"usr:a", "a", "a.cpp", "hash-a", true, false, {}, {}, {}}},
|
|
{{"usr:a", "usr:b", fold::index::EdgeKind::Conservative, 0.5}});
|
|
index.replaceTranslationUnit("b.cpp",
|
|
{{"usr:b", "b", "b.cpp", "hash-b", true, false, {}, {}, {}}},
|
|
{{"usr:b", "usr:c", fold::index::EdgeKind::Unknown, 0.0}});
|
|
index.replaceTranslationUnit("c.cpp",
|
|
{{"usr:c", "c", "c.cpp", "hash-c", true, false, {}, {}, {}}}, {});
|
|
|
|
CHECK(index.reverseDependents({"usr:c"}) == std::vector<std::string>{"usr:a", "usr:b"});
|
|
|
|
const auto unchanged = index.replaceTranslationUnit(
|
|
"a.cpp", {{"usr:a", "a", "a.cpp", "hash-a", true, false, {}, {}, {}}},
|
|
{{"usr:a", "usr:b", fold::index::EdgeKind::Conservative, 0.5}});
|
|
CHECK(unchanged.reused_functions == 1);
|
|
CHECK(unchanged.changed_functions == 0);
|
|
|
|
const auto changed = index.replaceTranslationUnit(
|
|
"a.cpp",
|
|
{{"usr:a", "a", "a.cpp", "hash-a2", true, false, {}, {}, {}},
|
|
{"usr:new", "new_function", "a.cpp", "hash-new", true, false, {}, {}, {}}},
|
|
{});
|
|
CHECK(changed.changed_functions == 2);
|
|
CHECK(changed.reused_functions == 0);
|
|
CHECK(changed.removed_functions == 0);
|
|
CHECK(index.outgoingEdges("usr:a").empty());
|
|
}
|
|
|
|
TEST_CASE("project index chunks reverse-dependents past sqlite compound-select limit",
|
|
"[project-index][incremental][large]") {
|
|
// A fresh or large incremental scan changes more than 500 USRs; the
|
|
// recursive-CTE seed list then exceeds SQLite's 500-term compound SELECT
|
|
// cap (SQLITE_MAX_COMPOUND_SELECT) and the update must still succeed with
|
|
// the exact transitive dependents.
|
|
fold::index::ProjectIndex index(":memory:");
|
|
REQUIRE(index.isOpen());
|
|
|
|
// reverseDependents walks reverse edges (callers of changed functions), so
|
|
// build a fan-in closure: g_i -> f_i and h_i -> g_i. Changing the 600 f_*
|
|
// bodies seeds 600 USRs and must invalidate exactly {g_*, h_*}.
|
|
constexpr int kChanged = 600;
|
|
constexpr int kFunctions = 3 * kChanged;
|
|
const auto build_closure = [&](bool mutate_functions) {
|
|
fold::ir::ProjectCallGraph graph;
|
|
const auto usr = [](const char* prefix, int i) {
|
|
return std::string("usr:") + prefix + std::to_string(i);
|
|
};
|
|
const auto file_of = [](int i) { return i < kChanged ? "a.cpp" : "b.cpp"; };
|
|
for (int i = 0; i < kChanged; ++i) {
|
|
const std::string body =
|
|
mutate_functions ? "body2-" + std::to_string(i) : "body-" + std::to_string(i);
|
|
graph.addFunction(usr("f", i), "f" + std::to_string(i), file_of(i), true, body);
|
|
graph.addFunction(usr("g", i), "g" + std::to_string(i), file_of(i), true,
|
|
"body-g-" + std::to_string(i));
|
|
graph.addFunction(usr("h", i), "h" + std::to_string(i), file_of(i), true,
|
|
"body-h-" + std::to_string(i));
|
|
graph.addCallEdge(usr("g", i), usr("f", i), fold::ir::CallGraphEdgeKind::Direct, 1.0);
|
|
graph.addCallEdge(usr("h", i), usr("g", i), fold::ir::CallGraphEdgeKind::Direct, 1.0);
|
|
}
|
|
graph.computeSCCs();
|
|
return graph;
|
|
};
|
|
const std::vector<std::pair<std::string, std::string>> inputs = {{"file:a.cpp", "hash-1"},
|
|
{"file:b.cpp", "hash-2"}};
|
|
|
|
const auto cold = index.replaceProjectGraph(build_closure(false), inputs);
|
|
CHECK(cold.changed_functions == kFunctions);
|
|
CHECK(index.error().empty());
|
|
// Every function is a seed, so nothing can be a non-seed dependent.
|
|
CHECK(cold.invalidated_usrs.empty());
|
|
|
|
// 600 changed functions -> 600 CTE seed terms, previously > the 500-term
|
|
// compound-SELECT cap (prepare failed, error_ set, invalidated_usrs empty).
|
|
const auto update = index.replaceProjectGraph(build_closure(true));
|
|
CHECK(index.error().empty());
|
|
CHECK(update.changed_functions == kChanged);
|
|
CHECK(update.reused_functions == 2 * kChanged);
|
|
// Dependents of {f0..f599} = {g0..g599} U {h0..h599} (multi-hop closure).
|
|
REQUIRE(update.invalidated_usrs.size() == 2 * kChanged);
|
|
CHECK(update.invalidated_usrs.front() == "usr:g0");
|
|
CHECK(update.invalidated_usrs.back() == "usr:h99");
|
|
CHECK(std::is_sorted(update.invalidated_usrs.begin(), update.invalidated_usrs.end()));
|
|
CHECK(index.error().empty());
|
|
|
|
// Warm reuse round-trips identically and stays error-free.
|
|
const auto warm = index.replaceProjectGraph(build_closure(true), inputs);
|
|
CHECK(index.error().empty());
|
|
CHECK(warm.reused_functions == kFunctions);
|
|
CHECK(warm.changed_functions == 0);
|
|
CHECK(warm.invalidated_usrs.empty());
|
|
fold::ir::ProjectCallGraph loaded;
|
|
REQUIRE(index.loadProjectGraph(loaded));
|
|
CHECK(loaded.nodeCount() == kFunctions);
|
|
}
|
|
|
|
TEST_CASE("contribution merge resolves unresolved calls cross-TU like the exhaustive builder",
|
|
"[project-index][merge][resolution]") {
|
|
using fold::index::EdgeKind;
|
|
using fold::index::TranslationUnitGraphContribution;
|
|
using fold::index::UnresolvedCallKind;
|
|
|
|
const auto base = [](std::string usr, std::string name) {
|
|
return fold::index::FunctionRecord{usr,
|
|
name,
|
|
"a.cpp",
|
|
"body-" + usr,
|
|
true,
|
|
false,
|
|
0,
|
|
"void (int)",
|
|
std::vector<std::string>{}};
|
|
};
|
|
const std::string family_key = "Base::run|void (int)";
|
|
|
|
TranslationUnitGraphContribution a;
|
|
a.fingerprint = {.source_path = "a.cpp",
|
|
.compile_command_hash = "cc-a",
|
|
.source_hash = "sh-a",
|
|
.toolchain_hash = "tc",
|
|
.configuration_profile_hash = fold::index::kNoConfigurationProfileHash,
|
|
.schema_version = fold::index::kGraphContributionSchemaVersion,
|
|
.dependencies = {{"a.cpp", "sh-a", fold::index::DependencyKind::Source}}};
|
|
auto caller_record = base("usr:caller", "caller");
|
|
caller_record.canonical_signature = "void (void)";
|
|
a.functions.push_back(caller_record);
|
|
a.unresolved_calls.push_back({.callsite_id = "a.cpp:1:1",
|
|
.caller_usr = "usr:caller",
|
|
.kind = UnresolvedCallKind::Virtual,
|
|
.canonical_signature = {},
|
|
.direct_target_usr = "usr:Base::run",
|
|
.override_family_keys = {family_key}});
|
|
a.unresolved_calls.push_back({.callsite_id = "a.cpp:2:1",
|
|
.caller_usr = "usr:caller",
|
|
.kind = UnresolvedCallKind::Indirect,
|
|
.canonical_signature = "void (int)",
|
|
.direct_target_usr = {},
|
|
.override_family_keys = {}});
|
|
|
|
TranslationUnitGraphContribution b;
|
|
b.fingerprint = {.source_path = "b.cpp",
|
|
.compile_command_hash = "cc-b",
|
|
.source_hash = "sh-b",
|
|
.toolchain_hash = "tc",
|
|
.configuration_profile_hash = fold::index::kNoConfigurationProfileHash,
|
|
.schema_version = fold::index::kGraphContributionSchemaVersion,
|
|
.dependencies = {{"b.cpp", "sh-b", fold::index::DependencyKind::Source}}};
|
|
auto base_run = base("usr:Base::run", "Base::run");
|
|
base_run.override_family_keys = {family_key};
|
|
auto derived_run = base("usr:Derived::run", "Derived::run");
|
|
derived_run.override_family_keys = {family_key};
|
|
auto handler = base("usr:handler", "handler");
|
|
handler.canonical_signature = "void (int)";
|
|
b.functions = {base_run, derived_run, handler};
|
|
|
|
// Strict merge (the non-reachability path that previously forced the
|
|
// exhaustive fallback): resolution matches both family targets and the
|
|
// signature target; nothing is refused.
|
|
fold::ir::ProjectCallGraph graph;
|
|
std::string reason;
|
|
REQUIRE(fold::cli::mergeTranslationUnitGraphContributions({a, b}, graph, reason,
|
|
/*allow_unresolved_calls=*/false));
|
|
CHECK(reason.empty());
|
|
CHECK(graph.edgeCount(fold::ir::CallGraphEdgeKind::Conservative) == 3);
|
|
CHECK(graph.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) == 0);
|
|
const auto callees = graph.getCalleeIds("usr:caller");
|
|
REQUIRE(callees.size() == 3);
|
|
CHECK(std::find(callees.begin(), callees.end(), "usr:Base::run") != callees.end());
|
|
CHECK(std::find(callees.begin(), callees.end(), "usr:Derived::run") != callees.end());
|
|
CHECK(std::find(callees.begin(), callees.end(), "usr:handler") != callees.end());
|
|
|
|
// A genuinely unresolvable indirect call degrades to an Unknown synthetic
|
|
// edge, exactly like the exhaustive builder.
|
|
TranslationUnitGraphContribution lone;
|
|
lone.fingerprint = {.source_path = "c.cpp",
|
|
.compile_command_hash = "cc-c",
|
|
.source_hash = "sh-c",
|
|
.toolchain_hash = "tc",
|
|
.configuration_profile_hash = fold::index::kNoConfigurationProfileHash,
|
|
.schema_version = fold::index::kGraphContributionSchemaVersion,
|
|
.dependencies = {{"c.cpp", "sh-c", fold::index::DependencyKind::Source}}};
|
|
lone.functions.push_back(base("usr:lone", "lone"));
|
|
lone.unresolved_calls.push_back({.callsite_id = "c.cpp:1:1",
|
|
.caller_usr = "usr:lone",
|
|
.kind = UnresolvedCallKind::Indirect,
|
|
.canonical_signature = "int (void)",
|
|
.direct_target_usr = {},
|
|
.override_family_keys = {}});
|
|
fold::ir::ProjectCallGraph partial;
|
|
REQUIRE(fold::cli::mergeTranslationUnitGraphContributions({lone}, partial, reason,
|
|
/*allow_unresolved_calls=*/false));
|
|
CHECK(partial.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) == 1);
|
|
CHECK(partial.unresolvedNodeCount() == 1);
|
|
|
|
// allow_unresolved_calls=true (reachability-only path) keeps dropping
|
|
// unresolved evidence without synthetic nodes.
|
|
fold::ir::ProjectCallGraph loose;
|
|
REQUIRE(fold::cli::mergeTranslationUnitGraphContributions({lone}, loose, reason,
|
|
/*allow_unresolved_calls=*/true));
|
|
CHECK(loose.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) == 0);
|
|
CHECK(loose.edgeCount() == 0);
|
|
}
|
|
|
|
TEST_CASE("contribution merge tolerates same-USR spelling variants like the exhaustive builder",
|
|
"[project-index][merge][usr-conflict]") {
|
|
using fold::index::TranslationUnitGraphContribution;
|
|
|
|
// Real-world case (fold optimization loop iteration 3): libc++ inline
|
|
// template/inline functions appear in multiple TU contributions with the
|
|
// same semantic USR but spelling-variant qualified names (std:: vs
|
|
// std::__1) and sometimes an empty body hash (TU-dependent instantiation).
|
|
// ProjectCallGraph::addFunction — the exhaustive builder this merge must
|
|
// mirror — merges those duplicates; the merge must not refuse evidence
|
|
// the fallback tolerates.
|
|
const auto make_contribution = [](const std::string& path, const std::string& qualified_name,
|
|
const std::string& body_hash) {
|
|
TranslationUnitGraphContribution c;
|
|
c.fingerprint = {
|
|
.source_path = path,
|
|
.compile_command_hash = "cc-" + path,
|
|
.source_hash = "sh-" + path,
|
|
.toolchain_hash = "tc",
|
|
.configuration_profile_hash = fold::index::kNoConfigurationProfileHash,
|
|
.schema_version = fold::index::kGraphContributionSchemaVersion,
|
|
.dependencies = {{path, "sh-" + path, fold::index::DependencyKind::Source}}};
|
|
c.functions.push_back({"usr:libcpp-fn",
|
|
qualified_name,
|
|
"/sdk/usr/include/c++/v1/header.h",
|
|
body_hash,
|
|
true,
|
|
false,
|
|
0,
|
|
{},
|
|
{}});
|
|
c.functions.push_back(
|
|
{"usr:caller", "caller", path, "body-caller", true, false, 0, {}, {}});
|
|
return c;
|
|
};
|
|
|
|
auto a = make_contribution("a.cpp", "std::__1::operator<<", "7249926610571155715");
|
|
auto b = make_contribution("b.cpp", "std::operator<<", "7249926610571155715");
|
|
auto c = make_contribution("c.cpp", "std::operator<<", "");
|
|
|
|
fold::ir::ProjectCallGraph graph;
|
|
std::string reason;
|
|
REQUIRE(fold::cli::mergeTranslationUnitGraphContributions({a, b, c}, graph, reason,
|
|
/*allow_unresolved_calls=*/false));
|
|
CHECK(reason.empty());
|
|
const auto* merged = graph.getNode("usr:libcpp-fn");
|
|
REQUIRE(merged != nullptr);
|
|
CHECK(merged->is_defined);
|
|
CHECK(!merged->body_hash.empty());
|
|
}
|
|
|
|
TEST_CASE("contribution store round-trips cross-TU resolution facts",
|
|
"[project-index][tu-contribution][resolution]") {
|
|
const auto path = temporaryDatabase();
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.isOpen());
|
|
fold::index::TranslationUnitGraphContribution contribution;
|
|
contribution.fingerprint = {.source_path = "a.cpp",
|
|
.compile_command_hash = "cc-a",
|
|
.source_hash = "sh-a",
|
|
.toolchain_hash = "tc",
|
|
.configuration_profile_hash =
|
|
fold::index::kNoConfigurationProfileHash,
|
|
.schema_version = fold::index::kGraphContributionSchemaVersion,
|
|
.dependencies = {}};
|
|
contribution.functions.push_back({"usr:Base::run",
|
|
"Base::run",
|
|
"a.cpp",
|
|
"body",
|
|
true,
|
|
false,
|
|
0,
|
|
"void (int)",
|
|
{"Base::run|void (int)", "Base::run|void (long)"}});
|
|
REQUIRE(index.replaceTranslationUnitContribution(contribution));
|
|
}
|
|
{
|
|
fold::index::ProjectIndex index(path.string());
|
|
REQUIRE(index.isOpen());
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok);
|
|
REQUIRE(loaded.contributions.size() == 1);
|
|
REQUIRE(loaded.contributions[0].functions.size() == 1);
|
|
const auto& function = loaded.contributions[0].functions[0];
|
|
CHECK(function.canonical_signature == "void (int)");
|
|
CHECK(function.override_family_keys ==
|
|
std::vector<std::string>{"Base::run|void (int)", "Base::run|void (long)"});
|
|
}
|
|
std::filesystem::remove(path);
|
|
}
|