From 7eaa4991a496d458e6330e8491e74baceaba2e0a Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Costa Date: Sat, 11 Oct 2025 19:50:59 +0000 Subject: [PATCH] fix(libstore/find-cycles): add second pass to merge multiedges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single-pass greedy algorithm could fail to connect all edges if they arrived in certain orders. For example, edges A→B, C→D, D→A, B→C would result in two paths [D→A→B→C] and [C→D] instead of one complete cycle. Added a second pass that repeatedly tries to merge existing multiedges with each other until no more merges are possible. This ensures we find complete cycle paths regardless of edge discovery order. --- src/libstore/build/find-cycles.cc | 39 ++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/libstore/build/find-cycles.cc b/src/libstore/build/find-cycles.cc index 7447064c1..517a1f5a3 100644 --- a/src/libstore/build/find-cycles.cc +++ b/src/libstore/build/find-cycles.cc @@ -155,20 +155,16 @@ void transformEdgesToMultiedges(StoreCycleEdgeVec & edges, StoreCycleEdgeVec & m { debug("transformEdgesToMultiedges: processing %lu edges", edges.size()); + // First pass: join edges to multiedges for (auto & edge2 : edges) { bool edge2Joined = false; for (auto & edge1 : multiedges) { - debug("comparing edge1 (size=%lu) with edge2 (size=%lu)", edge1.size(), edge2.size()); - // Check if edge1.back() == edge2.front() // This means we can extend edge1 by appending edge2 if (edge1.back() == edge2.front()) { - debug("appending: edge1.back()='%s' == edge2.front()='%s'", edge1.back(), edge2.front()); - // Append all but the first element of edge2 (to avoid duplication) for (size_t i = 1; i < edge2.size(); i++) { - debug(" appending edge2[%lu] = %s", i, edge2[i]); edge1.push_back(edge2[i]); } edge2Joined = true; @@ -178,11 +174,8 @@ void transformEdgesToMultiedges(StoreCycleEdgeVec & edges, StoreCycleEdgeVec & m // Check if edge2.back() == edge1.front() // This means we can extend edge1 by prepending edge2 if (edge2.back() == edge1.front()) { - debug("prepending: edge2.back()='%s' == edge1.front()='%s'", edge2.back(), edge1.front()); - // Prepend all but the last element of edge2 (to avoid duplication) for (int i = edge2.size() - 2; i >= 0; i--) { - debug(" prepending edge2[%d] = %s", i, edge2[i]); edge1.push_front(edge2[i]); } edge2Joined = true; @@ -191,11 +184,39 @@ void transformEdgesToMultiedges(StoreCycleEdgeVec & edges, StoreCycleEdgeVec & m } if (!edge2Joined) { - debug("edge2 is new, adding as separate multiedge"); multiedges.push_back(edge2); } } + // Second pass: merge multiedges that can now be connected + // After joining edges, some multiedges might now be connectable + bool merged = true; + while (merged) { + merged = false; + for (size_t i = 0; i < multiedges.size() && !merged; i++) { + for (size_t j = i + 1; j < multiedges.size() && !merged; j++) { + auto & path1 = multiedges[i]; + auto & path2 = multiedges[j]; + + if (path1.back() == path2.front()) { + // Append path2 to path1 + for (size_t k = 1; k < path2.size(); k++) { + path1.push_back(path2[k]); + } + multiedges.erase(multiedges.begin() + j); + merged = true; + } else if (path2.back() == path1.front()) { + // Prepend path2 to path1 + for (int k = path2.size() - 2; k >= 0; k--) { + path1.push_front(path2[k]); + } + multiedges.erase(multiedges.begin() + j); + merged = true; + } + } + } + } + debug("transformEdgesToMultiedges: result has %lu multiedges", multiedges.size()); }