Skip to content

Commit 5f20191

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 b0b6e76 commit 5f20191

File tree

6 files changed

+411
-73
lines changed

6 files changed

+411
-73
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: 136 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,66 @@ extension BuildPlan {
905905
extension BuildPlan {
906906
fileprivate typealias Destination = BuildParameters.Destination
907907

908+
fileprivate enum TraversalNode: Hashable {
909+
case package(ResolvedPackage)
910+
case product(ResolvedProduct, BuildParameters.Destination)
911+
case module(ResolvedModule, BuildParameters.Destination)
912+
913+
var destination: BuildParameters.Destination {
914+
switch self {
915+
case .package:
916+
.target
917+
case .product(_, let destination):
918+
destination
919+
case .module(_, let destination):
920+
destination
921+
}
922+
}
923+
924+
init(
925+
product: ResolvedProduct,
926+
context destination: BuildParameters.Destination
927+
) {
928+
switch product.type {
929+
case .macro, .plugin:
930+
self = .product(product, .host)
931+
case .test:
932+
self = .product(product, product.modules.contains(where: Self.hasMacroDependency) ? .host : destination)
933+
default:
934+
self = .product(product, destination)
935+
}
936+
}
937+
938+
init(
939+
module: ResolvedModule,
940+
context destination: BuildParameters.Destination
941+
) {
942+
switch module.type {
943+
case .macro, .plugin:
944+
// Macros and plugins are ways built for host
945+
self = .module(module, .host)
946+
case .test:
947+
self = .module(module, Self.hasMacroDependency(module: module) ? .host : destination)
948+
default:
949+
// By default assume the destination of the context.
950+
// This means that i.e. test products that reference macros
951+
// would force all of their successors to be `host`
952+
self = .module(module, destination)
953+
}
954+
}
955+
956+
static func hasMacroDependency(module: ResolvedModule) -> Bool {
957+
module.dependencies.contains(where: {
958+
switch $0 {
959+
case .product(let productDependency, _):
960+
productDependency.type == .macro
961+
case .module(let moduleDependency, _):
962+
moduleDependency.type == .macro
963+
}
964+
})
965+
}
966+
}
967+
908968
/// Traverse the modules graph and find a destination for every product and module.
909969
/// All non-macro/plugin products and modules have `target` destination with one
910970
/// notable exception - test products/modules with direct macro dependency.
@@ -913,65 +973,16 @@ extension BuildPlan {
913973
onProduct: (ResolvedProduct, Destination) throws -> Void,
914974
onModule: (ResolvedModule, Destination) async throws -> Void
915975
) async rethrows {
916-
enum Node: Hashable {
917-
case package(ResolvedPackage)
918-
case product(ResolvedProduct, Destination)
919-
case module(ResolvedModule, Destination)
920-
921-
static func `for`(
922-
product: ResolvedProduct,
923-
context destination: Destination
924-
) -> Node {
925-
switch product.type {
926-
case .macro, .plugin:
927-
.product(product, .host)
928-
case .test:
929-
.product(product, product.modules.contains(where: self.hasMacroDependency) ? .host : destination)
930-
default:
931-
.product(product, destination)
932-
}
933-
}
934-
935-
static func `for`(
936-
module: ResolvedModule,
937-
context destination: Destination
938-
) -> Node {
939-
switch module.type {
940-
case .macro, .plugin:
941-
// Macros and plugins are ways built for host
942-
.module(module, .host)
943-
case .test:
944-
.module(module, self.hasMacroDependency(module: module) ? .host : destination)
945-
default:
946-
// By default assume the destination of the context.
947-
// This means that i.e. test products that reference macros
948-
// would force all of their successors to be `host`
949-
.module(module, destination)
950-
}
951-
}
952-
953-
static func hasMacroDependency(module: ResolvedModule) -> Bool {
954-
module.dependencies.contains(where: {
955-
switch $0 {
956-
case .product(let productDependency, _):
957-
productDependency.type == .macro
958-
case .module(let moduleDependency, _):
959-
moduleDependency.type == .macro
960-
}
961-
})
962-
}
963-
}
964-
965-
func successors(for package: ResolvedPackage) -> [Node] {
966-
var successors: [Node] = []
976+
func successors(for package: ResolvedPackage) -> [TraversalNode] {
977+
var successors: [TraversalNode] = []
967978
for product in package.products {
968979
if case .test = product.underlying.type,
969980
!graph.rootPackages.contains(id: package.id)
970981
{
971982
continue
972983
}
973984

974-
successors.append(.for(product: product, context: .target))
985+
successors.append(.init(product: product, context: .target))
975986
}
976987

977988
for module in package.modules {
@@ -981,7 +992,7 @@ extension BuildPlan {
981992
continue
982993
}
983994

984-
successors.append(.for(module: module, context: .target))
995+
successors.append(.init(module: module, context: .target))
985996
}
986997

987998
return successors
@@ -990,35 +1001,35 @@ extension BuildPlan {
9901001
func successors(
9911002
for product: ResolvedProduct,
9921003
destination: Destination
993-
) -> [Node] {
1004+
) -> [TraversalNode] {
9941005
guard destination == .host else {
9951006
return []
9961007
}
9971008

9981009
return product.modules.map { module in
999-
.for(module: module, context: destination)
1010+
TraversalNode(module: module, context: destination)
10001011
}
10011012
}
10021013

10031014
func successors(
10041015
for module: ResolvedModule,
10051016
destination: Destination
1006-
) -> [Node] {
1017+
) -> [TraversalNode] {
10071018
guard destination == .host else {
10081019
return []
10091020
}
10101021

1011-
return module.dependencies.reduce(into: [Node]()) { partial, dependency in
1022+
return module.dependencies.reduce(into: [TraversalNode]()) { partial, dependency in
10121023
switch dependency {
10131024
case .product(let product, conditions: _):
1014-
partial.append(.for(product: product, context: destination))
1025+
partial.append(.init(product: product, context: destination))
10151026
case .module(let module, _):
1016-
partial.append(.for(module: module, context: destination))
1027+
partial.append(.init(module: module, context: destination))
10171028
}
10181029
}
10191030
}
10201031

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

10461122
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: Once 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)