Skip to content

Commit b010bb9

Browse files
committed
Allow each Int Set to specialize its creation of combinations
1 parent d9864ca commit b010bb9

File tree

2 files changed

+114
-8
lines changed

2 files changed

+114
-8
lines changed

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ extension PathHierarchy.DisambiguationContainer {
7373

7474
// Check if any columns are common for all overloads so that type name combinations with those columns can be skipped.
7575
let allOverloads = IntSet(0 ..< listOfOverloadTypeNames.count)
76-
let typeNameIndicesToCheck = (0 ..< numberOfTypes).filter {
76+
let typeNameIndicesToCheck = IntSet((0 ..< numberOfTypes).filter {
7777
// It's sufficient to check the first row because this column has to be the same for all rows
7878
table[0][$0] != allOverloads
79-
}
79+
})
8080

8181
guard !typeNameIndicesToCheck.isEmpty else {
8282
// Every type name is common across the overloads. This information can't be used to disambiguate the overloads.
@@ -88,15 +88,16 @@ extension PathHierarchy.DisambiguationContainer {
8888
var shortestDisambiguationSoFar: (indicesToInclude: IntSet, length: Int)?
8989

9090
// For each overload we iterate over the possible parameter combinations with increasing number of elements in each combination.
91-
for typeNamesToInclude in typeNameIndicesToCheck.combinations(ofCount: 1...) {
91+
for typeNamesToInclude in typeNameIndicesToCheck.combinationsToCheck() {
9292
// Stop if we've already found a match with fewer parameters than this
9393
guard typeNamesToInclude.count <= (shortestDisambiguationSoFar?.indicesToInclude.count ?? .max) else {
9494
break
9595
}
9696

97-
let firstTypeNameToInclude = typeNamesToInclude.first! // The generated `typeNamesToInclude` is never empty.
97+
var iterator = typeNamesToInclude.makeIterator()
98+
let firstTypeNameToInclude = iterator.next()! // The generated `typeNamesToInclude` is never empty.
9899
// Compute which other overloads this combinations of type names also could refer to.
99-
let overlap = typeNamesToInclude.dropFirst().reduce(into: table[row][firstTypeNameToInclude]) { partialResult, index in
100+
let overlap = IteratorSequence(iterator).reduce(into: table[row][firstTypeNameToInclude]) { partialResult, index in
100101
partialResult.formIntersection(table[row][index])
101102
}
102103

@@ -133,12 +134,36 @@ extension PathHierarchy.DisambiguationContainer {
133134
// MARK: Int Set
134135

135136
/// A private protocol that abstracts sets of integers.
136-
private protocol _IntSet: SetAlgebra<Int> {
137+
private protocol _IntSet: SetAlgebra<Int>, Sequence<Int> {
137138
// In addition to the general SetAlgebra, the code in this file checks the number of elements in the set.
138139
var count: Int { get }
140+
141+
// Let each type specialize the creation of possible combinations to check.
142+
associatedtype CombinationSequence: Sequence<Self>
143+
func combinationsToCheck() -> CombinationSequence
144+
}
145+
146+
147+
extension Set<Int>: _IntSet {
148+
func combinationsToCheck() -> some Sequence<Self> {
149+
// For `Set<Int>`, use the Swift Algorithms implementation to generate the possible combinations.
150+
self.combinations(ofCount: 1...).lazy.map { Set($0) }
151+
}
152+
}
153+
extension _TinySmallValueIntSet: _IntSet {
154+
func combinationsToCheck() -> [Self] {
155+
// For `_TinySmallValueIntSet`, leverage the fact that bits of an Int represent the possible combinations.
156+
let smallest = storage.trailingZeroBitCount
157+
return (1 ... storage >> smallest)
158+
.compactMap {
159+
let combination = Self(storage: UInt64($0 << smallest))
160+
// Filter out any combinations that include columns that are the same for all overloads
161+
return self.isSuperset(of: combination) ? combination : nil
162+
}
163+
// The bits of larger and larger Int values won't be in order of number of bits set, so we sort them.
164+
.sorted(by: { $0.count < $1.count })
165+
}
139166
}
140-
extension Set<Int>: _IntSet {}
141-
extension _TinySmallValueIntSet: _IntSet {}
142167

143168
/// A specialized set-algebra type that only stores the possible values `0 ..< 64`.
144169
///
@@ -233,3 +258,38 @@ struct _TinySmallValueIntSet: SetAlgebra {
233258
storage ^= other.storage
234259
}
235260
}
261+
262+
extension _TinySmallValueIntSet: Sequence {
263+
func makeIterator() -> Iterator {
264+
Iterator(set: self)
265+
}
266+
267+
struct Iterator: IteratorProtocol {
268+
typealias Element = Int
269+
270+
private let set: _TinySmallValueIntSet
271+
private var current: Int
272+
private let end: Int
273+
274+
@inlinable
275+
init(set: _TinySmallValueIntSet) {
276+
self.set = set
277+
self.current = set.storage.trailingZeroBitCount
278+
self.end = 64 - set.storage.leadingZeroBitCount
279+
}
280+
281+
@inlinable
282+
mutating func next() -> Int? {
283+
defer { current += 1 }
284+
285+
while !set.contains(current) {
286+
current += 1
287+
if end <= current {
288+
return nil
289+
}
290+
}
291+
292+
return current
293+
}
294+
}
295+
}

Tests/SwiftDocCTests/Infrastructure/TinySmallValueIntSetTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
import XCTest
12+
import Algorithms
1213
@testable import SwiftDocC
1314

1415
class TinySmallValueIntSetTests: XCTestCase {
@@ -67,4 +68,49 @@ class TinySmallValueIntSetTests: XCTestCase {
6768
XCTAssertEqual(tiny.contains(9), real.contains(9))
6869
XCTAssertEqual(tiny.count, real.count)
6970
}
71+
72+
func testCombinations() {
73+
do {
74+
let tiny: _TinySmallValueIntSet = [0,1,2]
75+
XCTAssertEqual(tiny.combinationsToCheck().map { $0.sorted() }, [
76+
[0], [1], [2],
77+
[0,1], [0,2], [1,2],
78+
[0,1,2]
79+
])
80+
}
81+
82+
do {
83+
let tiny: _TinySmallValueIntSet = [2,5,9]
84+
XCTAssertEqual(tiny.combinationsToCheck().map { $0.sorted() }, [
85+
[2], [5], [9],
86+
[2,5], [2,9], [5,9],
87+
[2,5,9]
88+
])
89+
}
90+
91+
do {
92+
let tiny: _TinySmallValueIntSet = [3,4,7,11,15,16]
93+
94+
let expected = Array(tiny).combinations(ofCount: 1...)
95+
let actual = tiny.combinationsToCheck().map { Array($0) }
96+
97+
XCTAssertEqual(expected.count, actual.count)
98+
99+
// The two implementations doesn't need to provide combinations in the same order within a size
100+
let expectedBySize: [[[Int]]] = expected.grouped(by: \.count).sorted(by: \.key).map(\.value)
101+
let actualBySize: [[[Int]]] = actual .grouped(by: \.count).sorted(by: \.key).map(\.value)
102+
103+
for (expectedForSize, actualForSize) in zip(expectedBySize, actualBySize) {
104+
XCTAssertEqual(expectedForSize.count, actualForSize.count)
105+
106+
// Comparing [Int] descriptions to allow each same-size combination list to have different orders.
107+
// For example, these two lists of combinations (with the last 2 elements swapped) are considered equivalent:
108+
// [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]
109+
// [1, 2, 3], [1, 2, 4], [2, 3, 4], [1, 3, 4]
110+
XCTAssertEqual(expectedForSize.map(\.description).sorted(),
111+
actualForSize .map(\.description).sorted())
112+
113+
}
114+
}
115+
}
70116
}

0 commit comments

Comments
 (0)