Skip to content

Commit d811faf

Browse files
authored
Add UndirectedGraph and DirectedGraph data structures (#7460)
`DirectedGraph` can be used to represent the existing modules graph in the `PackageGraph` module hopefully with better performance due to reduced number of allocations. `UndirectedGraph` could represent a new linkage graph concept, which would help with breaking dependency cycles for certain packages with products that don't have a linkage cycle. I started with an adjacency list in a directed graph implementation, but then proceeded to an undirected graph where adjacency matrix seemed obviously more performant when adding edges: just flipping two bits in the matrix instead of adding potential array (re-)allocations with the list. I'm unsure though if either is a clear win in all cases, adjacency matrix may be fast at adding edges, but not the fastest for different kinds of graphs analysis and lookups. Keeping both implementations for future reference.
1 parent d8c8d0f commit d811faf

File tree

7 files changed

+295
-0
lines changed

7 files changed

+295
-0
lines changed

Sources/Basics/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ add_library(Basics
3434
FileSystem/TemporaryFile.swift
3535
FileSystem/TSCAdapters.swift
3636
FileSystem/VFSOverlay.swift
37+
Graph/AdjacencyMatrix.swift
38+
Graph/DirectedGraph.swift
39+
Graph/UndirectedGraph.swift
3740
SourceControlURL.swift
3841
HTTPClient/HTTPClient.swift
3942
HTTPClient/HTTPClientConfiguration.swift
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// A matrix storing bits of `true`/`false` state for a given combination of row and column indices. Used as
14+
/// a square matrix indicating edges in graphs, where rows and columns are indices in a storage of graph's nodes.
15+
///
16+
/// For example, in a graph that contains 3 nodes `matrix[row: 1, column: 2]` evaluating to `true` means an edge
17+
/// between nodes with indices `1` and `2` exists. `matrix[row: 1, column: 2]` evaluating to `false` means that no
18+
/// edge exists.
19+
///
20+
/// See https://en.wikipedia.org/wiki/Adjacency_matrix for more details.
21+
struct AdjacencyMatrix {
22+
let columns: Int
23+
let rows: Int
24+
private var bytes: [UInt8]
25+
26+
/// Allocates a new bit matrix with a given size.
27+
/// - Parameters:
28+
/// - rows: Number of rows in the matrix.
29+
/// - columns: Number of columns in the matrix.
30+
init(rows: Int, columns: Int) {
31+
self.columns = columns
32+
self.rows = rows
33+
34+
let (quotient, remainder) = (rows * columns).quotientAndRemainder(dividingBy: 8)
35+
self.bytes = .init(repeating: 0, count: quotient + (remainder > 0 ? 1 : 0))
36+
}
37+
38+
var bitCount: Int {
39+
bytes.count * 8
40+
}
41+
42+
private func calculateOffsets(row: Int, column: Int) -> (byteOffset: Int, bitOffsetInByte: Int) {
43+
let totalBitOffset = row * columns + column
44+
return (byteOffset: totalBitOffset / 8, bitOffsetInByte: totalBitOffset % 8)
45+
}
46+
47+
subscript(row: Int, column: Int) -> Bool {
48+
get {
49+
let (byteOffset, bitOffsetInByte) = calculateOffsets(row: row, column: column)
50+
51+
let result = (self.bytes[byteOffset] >> bitOffsetInByte) & 1
52+
return result == 1
53+
}
54+
55+
set {
56+
let (byteOffset, bitOffsetInByte) = calculateOffsets(row: row, column: column)
57+
58+
self.bytes[byteOffset] |= 1 << bitOffsetInByte
59+
}
60+
}
61+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct DequeModule.Deque
14+
15+
/// Directed graph that stores edges in [adjacency lists](https://en.wikipedia.org/wiki/Adjacency_list).
16+
struct DirectedGraph<Node> {
17+
init(nodes: [Node]) {
18+
self.nodes = nodes
19+
self.edges = .init(repeating: [], count: nodes.count)
20+
}
21+
22+
private var nodes: [Node]
23+
private var edges: [[Int]]
24+
25+
mutating func addEdge(source: Int, destination: Int) {
26+
self.edges[source].append(destination)
27+
}
28+
29+
/// Checks whether a path via previously created edges between two given nodes exists.
30+
/// - Parameters:
31+
/// - source: `Index` of a node to start traversing edges from.
32+
/// - destination: `Index` of a node to which a path could exist via edges from `source`.
33+
/// - Returns: `true` if a path from `source` to `destination` exists, `false` otherwise.
34+
func areNodesConnected(source: Int, destination: Int) -> Bool {
35+
var todo = Deque<Int>([source])
36+
var done = Set<Int>()
37+
38+
while !todo.isEmpty {
39+
let nodeIndex = todo.removeFirst()
40+
41+
for reachableIndex in self.edges[nodeIndex] {
42+
if reachableIndex == destination {
43+
return true
44+
} else if !done.contains(reachableIndex) {
45+
todo.append(reachableIndex)
46+
}
47+
}
48+
49+
done.insert(nodeIndex)
50+
}
51+
52+
return false
53+
}
54+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct DequeModule.Deque
14+
15+
/// Undirected graph that stores edges in an [adjacency matrix](https://en.wikipedia.org/wiki/Adjacency_list).
16+
struct UndirectedGraph<Node> {
17+
init(nodes: [Node]) {
18+
self.nodes = nodes
19+
self.edges = .init(rows: nodes.count, columns: nodes.count)
20+
}
21+
22+
private var nodes: [Node]
23+
private var edges: AdjacencyMatrix
24+
25+
mutating func addEdge(source: Int, destination: Int) {
26+
// Adjacency matrix is symmetrical for undirected graphs.
27+
self.edges[source, destination] = true
28+
self.edges[destination, source] = true
29+
}
30+
31+
/// Checks whether a connection via previously created edges between two given nodes exists.
32+
/// - Parameters:
33+
/// - source: `Index` of a node to start traversing edges from.
34+
/// - destination: `Index` of a node to which a connection could exist via edges from `source`.
35+
/// - Returns: `true` if a path from `source` to `destination` exists, `false` otherwise.
36+
func areNodesConnected(source: Int, destination: Int) -> Bool {
37+
var todo = Deque<Int>([source])
38+
var done = Set<Int>()
39+
40+
while !todo.isEmpty {
41+
let nodeIndex = todo.removeFirst()
42+
43+
for reachableIndex in self.edges.nodesAdjacentTo(nodeIndex) {
44+
if reachableIndex == destination {
45+
return true
46+
} else if !done.contains(reachableIndex) {
47+
todo.append(reachableIndex)
48+
}
49+
}
50+
51+
done.insert(nodeIndex)
52+
}
53+
54+
return false
55+
}
56+
}
57+
58+
private extension AdjacencyMatrix {
59+
func nodesAdjacentTo(_ nodeIndex: Int) -> [Int] {
60+
var result = [Int]()
61+
62+
for i in 0..<self.rows where self[i, nodeIndex] {
63+
result.append(i)
64+
}
65+
66+
return result
67+
}
68+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
@testable import Basics
14+
import XCTest
15+
16+
final class AdjacencyMatrixTests: XCTestCase {
17+
func testEmpty() {
18+
var matrix = AdjacencyMatrix(rows: 0, columns: 0)
19+
XCTAssertEqual(matrix.bitCount, 0)
20+
21+
matrix = AdjacencyMatrix(rows: 0, columns: 42)
22+
XCTAssertEqual(matrix.bitCount, 0)
23+
24+
matrix = AdjacencyMatrix(rows: 42, columns: 0)
25+
XCTAssertEqual(matrix.bitCount, 0)
26+
}
27+
28+
func testBits() {
29+
for count in 1..<10 {
30+
var matrix = AdjacencyMatrix(rows: count, columns: count)
31+
for row in 0..<count {
32+
for column in 0..<count {
33+
XCTAssertFalse(matrix[row, column])
34+
matrix[row, column] = true
35+
XCTAssertTrue(matrix[row, column])
36+
}
37+
}
38+
}
39+
}
40+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
@testable import Basics
14+
import XCTest
15+
16+
final class DirectedGraphTests: XCTestCase {
17+
func testNodesConnection() {
18+
var graph = DirectedGraph(nodes: ["app1", "lib1", "lib2", "app2", "lib3"])
19+
graph.addEdge(source: 0, destination: 1)
20+
graph.addEdge(source: 1, destination: 2)
21+
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 2))
22+
XCTAssertFalse(graph.areNodesConnected(source: 2, destination: 0))
23+
24+
graph.addEdge(source: 0, destination: 4)
25+
graph.addEdge(source: 3, destination: 4)
26+
XCTAssertTrue(graph.areNodesConnected(source: 3, destination: 4))
27+
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 4))
28+
XCTAssertFalse(graph.areNodesConnected(source: 1, destination: 4))
29+
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 4))
30+
}
31+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
@testable import Basics
14+
import XCTest
15+
16+
final class UndirectedGraphTests: XCTestCase {
17+
func testNodesConnection() {
18+
var graph = UndirectedGraph(nodes: ["app1", "lib1", "lib2", "app2", "lib3", "app3"])
19+
graph.addEdge(source: 0, destination: 1)
20+
graph.addEdge(source: 1, destination: 2)
21+
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 2))
22+
XCTAssertTrue(graph.areNodesConnected(source: 2, destination: 0))
23+
24+
graph.addEdge(source: 0, destination: 4)
25+
graph.addEdge(source: 3, destination: 4)
26+
XCTAssertTrue(graph.areNodesConnected(source: 3, destination: 4))
27+
XCTAssertTrue(graph.areNodesConnected(source: 4, destination: 3))
28+
XCTAssertTrue(graph.areNodesConnected(source: 0, destination: 4))
29+
XCTAssertTrue(graph.areNodesConnected(source: 4, destination: 0))
30+
XCTAssertTrue(graph.areNodesConnected(source: 1, destination: 4))
31+
XCTAssertTrue(graph.areNodesConnected(source: 4, destination: 1))
32+
33+
for i in 0...4 {
34+
XCTAssertFalse(graph.areNodesConnected(source: i, destination: 5))
35+
XCTAssertFalse(graph.areNodesConnected(source: 5, destination: i))
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)