Skip to content

Commit 13b861c

Browse files
committed
[BuildPlan] Add a way to traverse the graph and compute destinations for all modules
This is going to be used by sourcekit-lsp to build a graph/dictionary of all the targets and depths they appear during package loading and replaces the need for topological sort and `buildTriple`.
1 parent 392a2a9 commit 13b861c

File tree

6 files changed

+382
-68
lines changed

6 files changed

+382
-68
lines changed

Sources/Basics/Graph/GraphAlgorithms.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,50 @@ public func depthFirstSearch<T: Hashable>(
8686
}
8787
}
8888
}
89+
90+
private struct TraversalNode<T: Hashable>: Hashable {
91+
let parent: T?
92+
let curr: T
93+
let depth: Int
94+
}
95+
96+
/// Implements a pre-order depth-first search that traverses the whole graph and
97+
/// doesn't distinguish between unique and duplicate nodes. The method expects
98+
/// the graph to be acyclic but doesn't check that.
99+
///
100+
/// - Parameters:
101+
/// - nodes: The list of input nodes to sort.
102+
/// - successors: A closure for fetching the successors of a particular node.
103+
/// - onNext: A callback to indicate the node currently being processed
104+
/// including its parent (if any) and its depth.
105+
///
106+
/// - Complexity: O(v + e) where (v, e) are the number of vertices and edges
107+
/// reachable from the input nodes via the relation.
108+
public func depthFirstSearch<T: Hashable>(
109+
_ nodes: [T],
110+
successors: (T) throws -> [T],
111+
onNext: (T, _ parent: T?, _ depth: Int) throws -> Void
112+
) rethrows {
113+
var stack = OrderedSet<TraversalNode<T>>()
114+
115+
for node in nodes {
116+
precondition(stack.isEmpty)
117+
stack.append(TraversalNode(parent: nil, curr: node, depth: 0))
118+
119+
while !stack.isEmpty {
120+
let node = stack.removeLast()
121+
122+
try onNext(node.curr, node.parent, node.depth)
123+
124+
for succ in try successors(node.curr) {
125+
stack.append(
126+
TraversalNode(
127+
parent: node.curr,
128+
curr: succ,
129+
depth: node.depth + 1
130+
)
131+
)
132+
}
133+
}
134+
}
135+
}

Sources/Build/BuildPlan/BuildPlan.swift

Lines changed: 131 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,66 @@ extension BuildPlan {
904904
extension BuildPlan {
905905
fileprivate typealias Destination = BuildParameters.Destination
906906

907+
fileprivate enum TraversalNode: Hashable {
908+
case package(ResolvedPackage)
909+
case product(ResolvedProduct, BuildParameters.Destination)
910+
case module(ResolvedModule, BuildParameters.Destination)
911+
912+
var destination: BuildParameters.Destination {
913+
switch self {
914+
case .package:
915+
.target
916+
case .product(_, let destination):
917+
destination
918+
case .module(_, let destination):
919+
destination
920+
}
921+
}
922+
923+
static func `for`(
924+
product: ResolvedProduct,
925+
context destination: BuildParameters.Destination
926+
) -> Self {
927+
switch product.type {
928+
case .macro, .plugin:
929+
.product(product, .host)
930+
case .test:
931+
.product(product, product.modules.contains(where: self.hasMacroDependency) ? .host : destination)
932+
default:
933+
.product(product, destination)
934+
}
935+
}
936+
937+
static func `for`(
938+
module: ResolvedModule,
939+
context destination: BuildParameters.Destination
940+
) -> Self {
941+
switch module.type {
942+
case .macro, .plugin:
943+
// Macros and plugins are ways built for host
944+
.module(module, .host)
945+
case .test:
946+
.module(module, self.hasMacroDependency(module: module) ? .host : destination)
947+
default:
948+
// By default assume the destination of the context.
949+
// This means that i.e. test products that reference macros
950+
// would force all of their successors to be `host`
951+
.module(module, destination)
952+
}
953+
}
954+
955+
static func hasMacroDependency(module: ResolvedModule) -> Bool {
956+
module.dependencies.contains(where: {
957+
switch $0 {
958+
case .product(let productDependency, _):
959+
productDependency.type == .macro
960+
case .module(let moduleDependency, _):
961+
moduleDependency.type == .macro
962+
}
963+
})
964+
}
965+
}
966+
907967
/// Traverse the modules graph and find a destination for every product and module.
908968
/// All non-macro/plugin products and modules have `target` destination with one
909969
/// notable exception - test products/modules with direct macro dependency.
@@ -912,57 +972,8 @@ extension BuildPlan {
912972
onProduct: (ResolvedProduct, Destination) throws -> Void,
913973
onModule: (ResolvedModule, Destination) async throws -> Void
914974
) async rethrows {
915-
enum Node: Hashable {
916-
case package(ResolvedPackage)
917-
case product(ResolvedProduct, Destination)
918-
case module(ResolvedModule, Destination)
919-
920-
static func `for`(
921-
product: ResolvedProduct,
922-
context destination: Destination
923-
) -> Node {
924-
switch product.type {
925-
case .macro, .plugin:
926-
.product(product, .host)
927-
case .test:
928-
.product(product, product.modules.contains(where: self.hasMacroDependency) ? .host : destination)
929-
default:
930-
.product(product, destination)
931-
}
932-
}
933-
934-
static func `for`(
935-
module: ResolvedModule,
936-
context destination: Destination
937-
) -> Node {
938-
switch module.type {
939-
case .macro, .plugin:
940-
// Macros and plugins are ways built for host
941-
.module(module, .host)
942-
case .test:
943-
.module(module, self.hasMacroDependency(module: module) ? .host : destination)
944-
default:
945-
// By default assume the destination of the context.
946-
// This means that i.e. test products that reference macros
947-
// would force all of their successors to be `host`
948-
.module(module, destination)
949-
}
950-
}
951-
952-
static func hasMacroDependency(module: ResolvedModule) -> Bool {
953-
module.dependencies.contains(where: {
954-
switch $0 {
955-
case .product(let productDependency, _):
956-
productDependency.type == .macro
957-
case .module(let moduleDependency, _):
958-
moduleDependency.type == .macro
959-
}
960-
})
961-
}
962-
}
963-
964-
func successors(for package: ResolvedPackage) -> [Node] {
965-
var successors: [Node] = []
975+
func successors(for package: ResolvedPackage) -> [TraversalNode] {
976+
var successors: [TraversalNode] = []
966977
for product in package.products {
967978
if case .test = product.underlying.type,
968979
!graph.rootPackages.contains(id: package.id)
@@ -989,7 +1000,7 @@ extension BuildPlan {
9891000
func successors(
9901001
for product: ResolvedProduct,
9911002
destination: Destination
992-
) -> [Node] {
1003+
) -> [TraversalNode] {
9931004
guard destination == .host else {
9941005
return []
9951006
}
@@ -1002,12 +1013,12 @@ extension BuildPlan {
10021013
func successors(
10031014
for module: ResolvedModule,
10041015
destination: Destination
1005-
) -> [Node] {
1016+
) -> [TraversalNode] {
10061017
guard destination == .host else {
10071018
return []
10081019
}
10091020

1010-
return module.dependencies.reduce(into: [Node]()) { partial, dependency in
1021+
return module.dependencies.reduce(into: [TraversalNode]()) { partial, dependency in
10111022
switch dependency {
10121023
case .product(let product, conditions: _):
10131024
partial.append(.for(product: product, context: destination))
@@ -1017,7 +1028,7 @@ extension BuildPlan {
10171028
}
10181029
}
10191030

1020-
try await depthFirstSearch(graph.packages.map { Node.package($0) }) { node in
1031+
try await depthFirstSearch(graph.packages.map { TraversalNode.package($0) }) { node in
10211032
switch node {
10221033
case .package(let package):
10231034
successors(for: package)
@@ -1040,6 +1051,71 @@ extension BuildPlan {
10401051
// No de-duplication is necessary we only want unique nodes.
10411052
}
10421053
}
1054+
1055+
/// Traverses the modules graph, computes destination of every module reference and
1056+
/// provides the data to the caller by means of `onModule` callback. The products
1057+
/// are completely transparent to this method and are represented by their module dependencies.
1058+
package func traverseModules(
1059+
_ onModule: (
1060+
(ResolvedModule, BuildParameters.Destination),
1061+
_ parent: (ResolvedModule, BuildParameters.Destination)?,
1062+
_ depth: Int
1063+
) -> Void
1064+
) {
1065+
func successors(for package: ResolvedPackage) -> [TraversalNode] {
1066+
package.modules.compactMap {
1067+
if case .test = $0.underlying.type,
1068+
!self.graph.rootPackages.contains(id: package.id)
1069+
{
1070+
return nil
1071+
}
1072+
return .for(module: $0, context: .target)
1073+
}
1074+
}
1075+
1076+
func successors(
1077+
for module: ResolvedModule,
1078+
destination: Destination
1079+
) -> [TraversalNode] {
1080+
module.dependencies.reduce(into: [TraversalNode]()) { partial, dependency in
1081+
switch dependency {
1082+
case .product(let product, conditions: _):
1083+
let parent: TraversalNode = .for(product: product, context: destination)
1084+
for module in product.modules {
1085+
partial.append(.for(module: module, context: parent.destination))
1086+
}
1087+
case .module(let module, _):
1088+
partial.append(.for(module: module, context: destination))
1089+
}
1090+
}
1091+
}
1092+
1093+
depthFirstSearch(self.graph.packages.map { TraversalNode.package($0) }) {
1094+
switch $0 {
1095+
case .package(let package):
1096+
successors(for: package)
1097+
case .module(let module, let destination):
1098+
successors(for: module, destination: destination)
1099+
case .product:
1100+
[]
1101+
}
1102+
} onNext: { current, parent, depth in
1103+
let parentModule: (ResolvedModule, BuildParameters.Destination)? = switch parent {
1104+
case .package, .product, nil:
1105+
nil
1106+
case .module(let module, let destination):
1107+
(module, destination)
1108+
}
1109+
1110+
switch current {
1111+
case .package, .product:
1112+
break
1113+
1114+
case .module(let module, let destination):
1115+
onModule((module, destination), parentModule, depth)
1116+
}
1117+
}
1118+
}
10431119
}
10441120

10451121
extension Basics.Diagnostic {

Sources/PackageGraph/ModulesGraph.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,6 @@ public struct ModulesGraph {
107107
/// Returns all the modules in the graph, regardless if they are reachable from the root modules or not.
108108
public private(set) var allModules: IdentifiableSet<ResolvedModule>
109109

110-
/// Returns all modules within the graph in topological order, starting with low-level modules (that have no
111-
/// dependencies).
112-
package var allModulesInTopologicalOrder: [ResolvedModule] {
113-
get throws {
114-
try topologicalSort(Array(allModules)) { $0.dependencies.compactMap { $0.module } }.reversed()
115-
}
116-
}
117-
118110
/// Returns all the products in the graph, regardless if they are reachable from the root modules or not.
119111
public private(set) var allProducts: IdentifiableSet<ResolvedProduct>
120112

Sources/SourceKitLSPAPI/BuildDescription.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,22 @@ public struct BuildDescription {
142142
}
143143
}
144144

145-
/// Returns all targets within the module graph in topological order, starting with low-level targets (that have no
146-
/// dependencies).
147-
public func allTargetsInTopologicalOrder(in modulesGraph: ModulesGraph) throws -> [BuildTarget] {
148-
try modulesGraph.allModulesInTopologicalOrder.compactMap {
149-
getBuildTarget(for: $0, in: modulesGraph)
145+
public func traverseModules(
146+
callback: (any BuildTarget, _ parent: (any BuildTarget)?, _ depth: Int) -> Void
147+
) {
148+
// TODO: One the `targetMap` is switched over to use `IdentifiableSet<ModuleBuildDescription>`
149+
// we can introduce `BuildPlan.description(ResolvedModule, BuildParameters.Destination)`
150+
// and start using that here.
151+
self.buildPlan.traverseModules { module, parent, depth in
152+
let parentDescription: (any BuildTarget)? = if let parent {
153+
getBuildTarget(for: parent.0, in: self.buildPlan.graph)
154+
} else {
155+
nil
156+
}
157+
158+
if let description = getBuildTarget(for: module.0, in: self.buildPlan.graph) {
159+
callback(description, parentDescription, depth)
160+
}
150161
}
151162
}
152163

0 commit comments

Comments
 (0)