From 3cfeeb64b96709ca284d845637c75f2f8c248f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Wed, 13 Nov 2024 15:15:11 +0100 Subject: [PATCH] Suggest the minimal type disambiguation when an overload doesn't have any unique types (#1087) * Suggested only the minimal type disambiguation rdar://136207880 * Support disambiguating using a mix of parameter types and return types * Skip checking columns that are common for all overloads * Use Swift Algorithms package for combinations * Use specialized Set implementation for few overloads and with types * Allow each Int Set to specialize its creation of combinations * Avoid mapping combinations for large sizes to Set * Avoid reallocations when generating "tiny int set" combinations * Avoid indexing into a nested array * Speed up _TinySmallValueIntSet iteration * Avoid accessing a Set twice to check if a value exist and remove it * Avoid temporary allocation when creating set of remaining node IDs Also, ignore "sparse" nodes (without IDs) * Avoid reallocating the collisions list * Use a custom `_TinySmallValueIntSet.isSuperset(of:)` implementation * Use `Table` instead of indexing into `[[String]]` * Avoid recomputing the type name combinations to check * Compare the type name lengths by number of UTF8 code units * Update code comments, variable names, and internal documentation * Avoid recomputing type name overlap * Fix Swift 5.9 compatibility * Initialize each `Table` element. Linux requires this. * Address code review feedback: - Use plural for local variable with array value - Explicitly initialize optional local variable with `nil` - Add assert about passing empty type names - Explain what `nil` return value means in local code comment - Add comment indicating where `_TinySmallValueIntSet` is defined - Use + to join two string instead of string interpolation - Use named arguments for `makeDisambiguation` closure * Add detailed comment with example about how to find the fewest type names that disambiguate an overload * Don't use swift-algorithm as a _local_ dependency in Swift.org CI * Add additional test for 70 parameter type disambiguation * Add additional test that overloads with all the same parameters fallback to hash disambiguation * Remove Swift Algorithms dependency. For the extremely rare case of overloads with more than 64 parameters we only try disambiguation by a single parameter type name. * Only try mixed type disambiguation when symbol has both parameters and return value --- .../PathHierarchy+DisambiguatedPaths.swift | 104 +++- ...ierarchy+TypeSignatureDisambiguation.swift | 480 ++++++++++++++++++ .../Infrastructure/PathHierarchyTests.swift | 416 ++++++++++++++- .../TinySmallValueIntSetTests.swift | 149 ++++++ 4 files changed, 1117 insertions(+), 32 deletions(-) create mode 100644 Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift create mode 100644 Tests/SwiftDocCTests/Infrastructure/TinySmallValueIntSetTests.swift diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift index 1f1dc72577..2289e135fc 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift @@ -189,7 +189,7 @@ extension PathHierarchy { extension PathHierarchy.DisambiguationContainer { static func disambiguatedValues( - for elements: some Sequence, + for elements: some Collection, includeLanguage: Bool = false, allowAdvancedDisambiguation: Bool = true ) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { @@ -203,13 +203,20 @@ extension PathHierarchy.DisambiguationContainer { } private static func _disambiguatedValues( - for elements: some Sequence, + for elements: some Collection, includeLanguage: Bool, allowAdvancedDisambiguation: Bool ) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = [] + // Assume that all elements will find a disambiguation (or close to it) + collisions.reserveCapacity(elements.count) - var remainingIDs = Set(elements.map(\.node.identifier)) + var remainingIDs = Set() + remainingIDs.reserveCapacity(elements.count) + for element in elements { + guard let id = element.node.identifier else { continue } + remainingIDs.insert(id) + } // Kind disambiguation is the most readable, so we start by checking if any element has a unique kind. let groupedByKind = [String?: [Element]](grouping: elements, by: \.kind) @@ -233,7 +240,9 @@ extension PathHierarchy.DisambiguationContainer { collisions += _disambiguateByTypeSignature( elementsThatSupportAdvancedDisambiguation, types: \.returnTypes, - makeDisambiguation: Disambiguation.returnTypes, + makeDisambiguation: { _, disambiguatingTypeNames in + .returnTypes(disambiguatingTypeNames) + }, remainingIDs: &remainingIDs ) if remainingIDs.isEmpty { @@ -243,7 +252,31 @@ extension PathHierarchy.DisambiguationContainer { collisions += _disambiguateByTypeSignature( elementsThatSupportAdvancedDisambiguation, types: \.parameterTypes, - makeDisambiguation: Disambiguation.parameterTypes, + makeDisambiguation: { _, disambiguatingTypeNames in + .parameterTypes(disambiguatingTypeNames) + }, + remainingIDs: &remainingIDs + ) + if remainingIDs.isEmpty { + return collisions + } + + collisions += _disambiguateByTypeSignature( + elementsThatSupportAdvancedDisambiguation, + types: { element in + guard let parameterTypes = element.parameterTypes, + !parameterTypes.isEmpty, + let returnTypes = element.returnTypes, + !returnTypes.isEmpty + else { + return nil + } + return parameterTypes + returnTypes + }, + makeDisambiguation: { element, disambiguatingTypeNames in + let numberOfReturnTypes = element.returnTypes?.count ?? 0 + return .mixedTypes(parameterTypes: disambiguatingTypeNames.dropLast(numberOfReturnTypes), returnTypes: disambiguatingTypeNames.suffix(numberOfReturnTypes)) + }, remainingIDs: &remainingIDs ) if remainingIDs.isEmpty { @@ -341,6 +374,8 @@ extension PathHierarchy.DisambiguationContainer { case parameterTypes([String]) /// This node is disambiguated by its return types. case returnTypes([String]) + /// This node is disambiguated by a mix of parameter types and return types. + case mixedTypes(parameterTypes: [String], returnTypes: [String]) /// Makes a new disambiguation suffix string. func makeSuffix() -> String { @@ -361,6 +396,9 @@ extension PathHierarchy.DisambiguationContainer { case .parameterTypes(let types): // For example: "-(String,_)" or "-(_,Int)"` (a certain parameter has a certain type), or "-()" (has no parameters). return "-(\(types.joined(separator: ",")))" + + case .mixedTypes(parameterTypes: let parameterTypes, returnTypes: let returnTypes): + return Self.parameterTypes(parameterTypes).makeSuffix() + Self.returnTypes(returnTypes).makeSuffix() } } @@ -373,7 +411,7 @@ extension PathHierarchy.DisambiguationContainer { return kind.map { .kind($0) } ?? self case .hash: return hash.map { .hash($0) } ?? self - case .parameterTypes, .returnTypes: + case .parameterTypes, .returnTypes, .mixedTypes: return self } } @@ -382,36 +420,46 @@ extension PathHierarchy.DisambiguationContainer { private static func _disambiguateByTypeSignature( _ elements: [Element], types: (Element) -> [String]?, - makeDisambiguation: ([String]) -> Disambiguation, - remainingIDs: inout Set + makeDisambiguation: (Element, [String]) -> Disambiguation, + remainingIDs: inout Set ) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = [] + // Assume that all elements will find a disambiguation (or close to it) + collisions.reserveCapacity(elements.count) - let groupedByTypeCount = [Int?: [Element]](grouping: elements, by: { types($0)?.count }) - for (typesCount, elements) in groupedByTypeCount { - guard let typesCount else { continue } - guard elements.count > 1 else { + typealias ElementAndTypeNames = (element: Element, typeNames: [String]) + var groupedByTypeCount: [Int: [ElementAndTypeNames]] = [:] + for element in elements { + guard let typeNames = types(element) else { continue } + + groupedByTypeCount[typeNames.count, default: []].append((element, typeNames)) + } + + for (numberOfTypeNames, elementAndTypeNamePairs) in groupedByTypeCount { + guard elementAndTypeNamePairs.count > 1 else { // Only one element has this number of types. Disambiguate with only underscores. - let element = elements.first! - guard remainingIDs.contains(element.node.identifier) else { continue } // Don't disambiguate the same element more than once - collisions.append((value: element.node, disambiguation: makeDisambiguation(.init(repeating: "_", count: typesCount)))) - remainingIDs.remove(element.node.identifier) + let (element, _) = elementAndTypeNamePairs.first! + guard remainingIDs.remove(element.node.identifier) != nil else { + continue // Don't disambiguate the same element more than once + } + collisions.append((value: element.node, disambiguation: makeDisambiguation(element, .init(repeating: "_", count: numberOfTypeNames)))) continue } - guard typesCount > 0 else { continue } // Need at least one return value to disambiguate - for typeIndex in 0.. 0 else { + continue // Need at least one type name to disambiguate (when there are multiple elements without parameters or return values) + } + + let suggestedDisambiguations = minimalSuggestedDisambiguation(forOverloadsAndTypeNames: elementAndTypeNamePairs) + + for (pair, disambiguation) in zip(elementAndTypeNamePairs, suggestedDisambiguations) { + guard let disambiguation else { + continue // This element can't be uniquely disambiguated using these types + } + guard remainingIDs.remove(pair.element.node.identifier) != nil else { + continue // Don't disambiguate the same element more than once } + collisions.append((value: pair.element.node, disambiguation: makeDisambiguation(pair.element, disambiguation))) } } return collisions diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift new file mode 100644 index 0000000000..336e4d853e --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift @@ -0,0 +1,480 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation + +extension PathHierarchy.DisambiguationContainer { + + /// Returns the minimal suggested type-signature disambiguation for a list of overloads with lists of type names (either parameter types or return value types). + /// + /// For example, the following type names + /// ``` + /// String Int Double + /// String? Int Double + /// String? Int Float + /// ``` + /// can be disambiguated using: + /// - `String,_,_` because only the first overload has `String` as its first type + /// - `String?,_,Double` because the combination of `String?` as its first type and `Double` as the last type is unique to the second overload. + /// - `_,_,Float` because only the last overload has `Float` as its last type. + /// + /// If an overload can't be disambiguated using the provided type names, the returned value for that index is `nil`. + /// + /// - Parameter overloadsAndTypeNames: The lists of overloads and their type-name lists to shrink to the minimal unique combinations of disambiguating type names. + /// - Returns: A list of the minimal unique combinations of disambiguating type names for each overload, or `nil` for a specific index if that overload can't be uniquely disambiguated using the provided type names. + /// + /// - Precondition: All overloads have the same number of type names, greater than 0. + static func minimalSuggestedDisambiguation(forOverloadsAndTypeNames overloadsAndTypeNames: [(element: Element, typeNames: [String])]) -> [[String]?] { + // The number of types in each list + guard let numberOfTypes = overloadsAndTypeNames.first?.typeNames.count, 0 < numberOfTypes else { + assertionFailure("Need at least one type name to disambiguate. It's the callers responsibility to check before calling this function.") + return [] + } + + guard overloadsAndTypeNames.dropFirst().allSatisfy({ $0.typeNames.count == numberOfTypes }) else { + assertionFailure("Overloads should always have the same number of type names (representing either parameter types or return types).") + return [] + } + + // Construct a table of the different overloads' type names for quick access. + let typeNames = Table(width: numberOfTypes, height: overloadsAndTypeNames.count) { buffer in + for (row, pair) in overloadsAndTypeNames.indexed() { + for (column, typeName) in pair.typeNames.indexed() { + buffer.initializeElementAt(row: row, column: column, to: typeName) + } + } + } + + if numberOfTypes < 64, overloadsAndTypeNames.count < 64 { + // If there are few enough types and few enough overloads, use an optimized implementation for finding the fewest and shortest combination + // of type names that uniquely disambiguates each overload. + return _minimalSuggestedDisambiguationForFewParameters(typeNames: typeNames) + } else { + // Otherwise, use a simpler implementation that only attempts to disambiguate each overload using a single type name. + // In practice, this should almost never happen since it's very rare to have overloads with more than 64 parameters or more than 64 overloads of the same symbol. + return _minimalSuggestedDisambiguationForManyParameters(typeNames: typeNames) + } + } + + private static func _minimalSuggestedDisambiguationForFewParameters(typeNames: Table) -> [[String]?] { + typealias IntSet = _TinySmallValueIntSet + // We find the minimal suggested type-signature disambiguation in two steps. + // + // First, we compute which type names occur in which overloads. + // For example, these type names (left) occur in these overloads (right). + // + // String Int Double [0 ] [012] [01 ] + // String? Int Double [ 12] [012] [01 ] + // String? Int Float [ 12] [012] [ 2] + let table = Table(width: typeNames.size.width, height: typeNames.size.height) { buffer in + for column in typeNames.columnIndices { + // When a type name is common across multiple overloads we don't need to recompute that information. + // For example, consider a column of these 5 type names: ["Int", "Double", "Int", "Bool", "Double"]. + // + // For the first type name ("Int"), we don't know anything about the other rows yet, so we check all 5. + // This finds that "Int" occurs in both rows 0 and row 2. This information tells us that: + // - we can assign `[0 2 ]` to both those rows + // - we we don't need to check either of those rows again for the other type names. + // + // Thus, for the next type name ("Double"), we know that it's not in row 0 or 2, so we only need to check rows 1, 3, and 4. + // This finds that "Double" occurs in both rows 1 and row 4, so we can assign `[ 1 4]` to both rows and don't check them again. + // + // Finally, for the third type name ("Bool") we know that it's not in rows 0, 1, 2, or 4, so we only need to check row 3. + // Since this is the only row to check we can assign `[ 3 ]` to it without iterating over any other rows. + // + // With no more rows to check we have found which type names occur in which overloads for every type name in this column. + + // At the start we need to consider every row + var rowsToCheck = IntSet(typeNames.rowIndices) + while !rowsToCheck.isEmpty { + // Find all the rows with this type name + var iterator = rowsToCheck.makeIterator() + let currentRow = iterator.next()! // Verified to not be empty above. + let typeName = typeNames[currentRow, column] + + var rowsWithThisTypeName = IntSet() + rowsWithThisTypeName.insert(currentRow) // We know that the type name exist on the current row + // Check all the other (unchecked rows) + while let row = iterator.next() { + guard typeNames[row, column] == typeName else { continue } + rowsWithThisTypeName.insert(row) + } + + // Once we've found which rows have this type name we can assign all of them... + for row in rowsWithThisTypeName { + // Assign all the rows ... + buffer.initializeElementAt(row: row, column: column, to: rowsWithThisTypeName) + } + // ... and we can remove them from `rowsToCheck` so we don't check them again for the next type name. + rowsToCheck.subtract(rowsWithThisTypeName) + } + } + } + + // Second, iterate over each overload and try different combinations of type names to find the shortest disambiguation. + // + // To reduce unnecessary work in the iteration, we precompute which type name combinations are meaningful to check. + + // Check if any columns are common for all overloads. Those type names won't meaningfully disambiguate any overload. + let allOverloads = IntSet(typeNames.rowIndices) + let typeNameIndicesToCheck = IntSet(typeNames.columnIndices.filter { + // It's sufficient to check the first row because this column has to be the same for all rows + table[0, $0] != allOverloads + }) + + guard !typeNameIndicesToCheck.isEmpty else { + // Every type name is common across all overloads. + // Return `nil` for each overload to indicate that none of them can be disambiguated using these type names. + return .init(repeating: nil, count: typeNames.size.width) + } + + // Create a sequence of type name combinations with increasing number of type names in each combination. + let typeNameCombinationsToCheck = typeNameIndicesToCheck.combinationsToCheck() + + return typeNames.rowIndices.map { row in + var shortestDisambiguationSoFar: (indicesToInclude: IntSet, length: Int)? = nil + + // To determine the fewest and shortest disambiguation for each overload, we check combinations with increasing number of type names. + // This explanation uses letters for type names occurrences to help distinguish them from the combinations of type names to check. + // + // For example, consider these type names from before (left) which occur in these overloads (right): + // + // String Int Double [A ] [ABC] [AB ] + // String? Int Double [ BC] [ABC] [AB ] + // String? Int Float [ BC] [ABC] [ C] + // + // With three different type names, the full list of combinations to check would be: + // + // [0 ] [ 1 ] [ 2] [01 ] [0 2] [ 12] [012] + // + // However, because the second type name [ 1 ] is known to be the same in all overloads, we can ignore any combination that includes it. + // This reduces the possible combinations to check down to: + // + // [0 ] ___ [ 2] ___ [0 2] ___ ___ + // + // For the first overload, we start by checking if the type names at [0 ], which is [A ] can disambiguate the overload. + // Because [A ] only contains one element, it can disambiguate the first overload. We calculate its length and keep track of this disambiguation. + // Next, we check the type names at [ 0], which is [AB ] for the first overload. This doesn't disambiguate the overload. + // Next, we look at the type names at [0 2]. Because these are two type names and we already have a disambiguation with one type name, + // we break out of the loop and return the type names at [0 ] as the disambiguation for this overload ("String", "_", "_"). + // + // For the second overload, we start over and check type names at [0 ], which is [ BC], can disambiguate the overload. + // It doesn't, so we check if the type names at [ 2], which is [AB ], can disambiguate the second overload. + // It also doesn't, so we check the if the type names at [0 2], which are [ BC] and [AB ], disambiguates the second overload. + // The intersection of [ BC] and [AB ] is [ B ] which only has one value, so it does disambiguate the overload. + // So, we break out of the loop and return the type names at [0 2] as the disambiguation for the second overload ("String?", "_", "Double"). + // + // The third overload works much like the first overload. The type names at [ 2], which is [ C], disambiguates the overload. + // So, we break before checking [0 2]--which would include more type names--and return the type names at [ 2] as the disambiguation ("_", "_", "Float"). + + for typeNamesToInclude in typeNameCombinationsToCheck { + // Stop if we've already found a disambiguating combination using fewer type names than this. + guard typeNamesToInclude.count <= (shortestDisambiguationSoFar?.indicesToInclude.count ?? .max) else { + break + } + + // Compute which other overloads this combinations of type names also could refer to. + var iterator = typeNamesToInclude.makeIterator() + let firstTypeNameToInclude = iterator.next()! // The generated `typeNamesToInclude` is never empty. + let overlap = IteratorSequence(iterator).reduce(into: table[row, firstTypeNameToInclude]) { accumulatedOverlap, index in + accumulatedOverlap.formIntersection(table[row, index]) + } + + guard overlap.count == 1 else { + // This combination of parameters doesn't disambiguate the result + continue + } + + // Track the combined length of this combination of type names in case another combination (with the same number of type names) is shorter. + let length = typeNamesToInclude.reduce(0) { accumulatedLength, index in + // It's faster to check the number of UTF8 code units. + // This disfavors non-UTF8 type names, but those could be harder to read/write so neither length is right or wrong here. + accumulatedLength + typeNames[row, index].utf8.count + } + if length < (shortestDisambiguationSoFar?.length ?? .max) { + shortestDisambiguationSoFar = (IntSet(typeNamesToInclude), length) + } + } + + guard let (indicesToInclude, _) = shortestDisambiguationSoFar else { + // This overload can't be uniquely disambiguated by these type names + return nil + } + + // Found the fewest (and shortest) type names that uniquely disambiguate this overload. + // Return the list of disambiguating type names or "_" for an unused type name. + return typeNames.columnIndices.map { + indicesToInclude.contains($0) ? typeNames[row, $0] : "_" + } + } + } + + private static func _minimalSuggestedDisambiguationForManyParameters(typeNames: Table) -> [[String]?] { + // If there are more than 64 parameters or more than 64 overloads we only try to disambiguate by a single type name. + // + // In practice, the number of parameters goes down rather quickly. + // After 16 parameters is's very rare to have symbols, let alone overloads. + // Overloads with more than 64 parameters or more than 64 overloads is exceptional. + // It could happen, but for the vast majority of projects, this code will never run. + // To keep the rest of the code simpler, we separate the code paths for few parameters and many parameters. + + return typeNames.rowIndices.map { row in + // With this many parameters, simply check if any single type name disambiguates each overload. + var shortestDisambiguationSoFar: (indexToInclude: Int, length: Int)? = nil + + for column in typeNames.columnIndices { + let typeName = typeNames[row, column] + + // Check if any other overload also has this type name at this location. + guard typeNames.rowIndices.allSatisfy({ $0 == row || typeNames[$0, column] != typeName }) else { + // This type name doesn't uniquely identify this overload. + continue + } + + // Track which disambiguating type name is the shortest. + let length = typeName.utf8.count + if length < (shortestDisambiguationSoFar?.length ?? .max) { + shortestDisambiguationSoFar = (column, length) + } + } + + guard let (indexToInclude, _) = shortestDisambiguationSoFar else { + // This overload can't be uniquely disambiguated by a single type name + return nil + } + + // Found the fewest (and shortest) type names that uniquely disambiguate this overload. + // Return the list of disambiguating type names or "_" for an unused type name. + return typeNames.columnIndices.map { + $0 == indexToInclude ? typeNames[row, $0] : "_" + } + } + } +} + +// MARK: Int Set + +/// A specialized set-algebra type that only stores the possible values `0 ..< 64`. +/// +/// This specialized implementation is _not_ suitable as a general purpose set-algebra type. +/// However, because the code in this file only works with consecutive sequences of very small integers (most likely `0 ..< 16` and increasingly less likely the higher the number), +/// and because the the sets of those integers is frequently accessed in loops, a specialized implementation addresses bottlenecks in `_minimalSuggestedDisambiguation(...)`. +/// +/// > Important: +/// > This type is thought of as file private but it made internal so that it can be tested. +struct _TinySmallValueIntSet: SetAlgebra { + typealias Element = Int + + init() {} + + @usableFromInline + private(set) var storage: UInt64 = 0 + + @inlinable + init(storage: UInt64) { + self.storage = storage + } + + private static func mask(_ number: Int) -> UInt64 { + precondition(number < 64, "Number \(number) is out of bounds (0..<64)") + return 1 << number + } + + @inlinable + @discardableResult + mutating func insert(_ member: Int) -> (inserted: Bool, memberAfterInsert: Int) { + let newStorage = storage | Self.mask(member) + defer { + storage = newStorage + } + return (newStorage != storage, member) + } + + @inlinable + @discardableResult + mutating func remove(_ member: Int) -> Int? { + let newStorage = storage & ~Self.mask(member) + defer { + storage = newStorage + } + return newStorage != storage ? member : nil + } + + @inlinable + @discardableResult + mutating func update(with member: Int) -> Int? { + let (inserted, _) = insert(member) + return inserted ? nil : member + } + + @inlinable + func contains(_ member: Int) -> Bool { + storage & Self.mask(member) != 0 + } + + @inlinable + var count: Int { + storage.nonzeroBitCount + } + + @inlinable + func isSuperset(of other: Self) -> Bool { + // Provide a custom implementation since this is called frequently in `combinationsToCheck()` + (storage & other.storage) == other.storage + } + + @inlinable + func union(_ other: Self) -> Self { + .init(storage: storage | other.storage) + } + + @inlinable + func intersection(_ other: Self) -> Self { + .init(storage: storage & other.storage) + } + + @inlinable + func symmetricDifference(_ other: Self) -> Self { + .init(storage: storage ^ other.storage) + } + + @inlinable + mutating func formUnion(_ other: Self) { + storage |= other.storage + } + + @inlinable + mutating func formIntersection(_ other: Self) { + storage &= other.storage + } + + @inlinable + mutating func formSymmetricDifference(_ other: Self) { + storage ^= other.storage + } +} + +extension _TinySmallValueIntSet: Sequence { + func makeIterator() -> Iterator { + Iterator(set: self) + } + + struct Iterator: IteratorProtocol { + typealias Element = Int + + private var storage: UInt64 + private var current: Int = -1 + + @inlinable + init(set: _TinySmallValueIntSet) { + self.storage = set.storage + } + + @inlinable + mutating func next() -> Int? { + guard storage != 0 else { + return nil + } + // If the set is somewhat sparse, we can find the next element faster by shifting to the next value. + // This saves needing to do `contains()` checks for all the numbers since the previous element. + let amountToShift = storage.trailingZeroBitCount + 1 + storage >>= amountToShift + + current += amountToShift + return current + } + } +} + +extension _TinySmallValueIntSet { + /// All possible combinations of values to check in order of increasing number of values. + func combinationsToCheck() -> [Self] { + // For `_TinySmallValueIntSet`, leverage the fact that bits of an Int represent the possible combinations. + let smallest = storage.trailingZeroBitCount + + var combinations: [Self] = [] + combinations.reserveCapacity((1 << count /*known to be <64 */) - 1) + + for raw in 1 ... storage >> smallest { + let combination = Self(storage: UInt64(raw << smallest)) + + // Filter out any combinations that include columns that are the same for all overloads + guard self.isSuperset(of: combination) else { continue } + + combinations.append(combination) + } + // The bits of larger and larger Int values won't be in order of number of bits set, so we sort them. + return combinations.sorted(by: { $0.count < $1.count }) + } +} + +// MARK: Table + +/// A fixed-size grid of elements. +private struct Table { + typealias Size = (width: Int, height: Int) + @usableFromInline + let size: Size + private let storage: ContiguousArray + + @inlinable + init(width: Int, height: Int, initializingWith initializer: (_ buffer: inout UnsafeMutableTableBufferPointer) throws -> Void) rethrows { + size = (width, height) + let capacity = width * height + storage = try .init(unsafeUninitializedCapacity: capacity) { buffer, initializedCount in + var wrappedBuffer = UnsafeMutableTableBufferPointer(width: width, wrapping: buffer) + try initializer(&wrappedBuffer) + initializedCount = capacity + } + } + + struct UnsafeMutableTableBufferPointer { + private let width: Int + private var wrapping: UnsafeMutableBufferPointer + + init(width: Int, wrapping: UnsafeMutableBufferPointer) { + self.width = width + self.wrapping = wrapping + } + + @inlinable + func initializeElementAt(row: Int, column: Int, to element: Element) { + wrapping.initializeElement(at: index(row: row, column: column), to: element) + } + + private func index(row: Int, column: Int) -> Int { + // Let the wrapped buffer validate the index + row * width + column + } + } + + @inlinable + subscript(row: Int, column: Int) -> Element { + _read { yield storage[index(row: row, column: column)] } + } + + private func index(row: Int, column: Int) -> Int { + // Give nice assertion messages in debug builds and let the wrapped array validate the index in release builds. + assert(0 <= row && row < size.height, "Row \(row) is out of range of 0..<\(size.height)") + assert(0 <= column && column < size.width, "Column \(column) is out of range of 0..<\(size.width)") + + return row * size.width + column + } + + @inlinable + var rowIndices: Range { + 0 ..< size.height + } + + @inlinable + var columnIndices: Range { + 0 ..< size.width + } +} diff --git a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift index 7fb04508e8..ac5330a7bc 100644 --- a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift @@ -880,7 +880,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual( // static func < (lhs: MyNumber, rhs: MyNumber) -> Bool paths["s:9Operators8MyNumberV1loiySbAC_ACtFZ"], - "/Operators/MyNumber/_(_:_:)-736gk") + "/Operators/MyNumber/_(_:_:)-(MyNumber,_)->Bool") XCTAssertEqual( // static func <= (lhs: Self, rhs: Self) -> Bool paths["s:SLsE2leoiySbx_xtFZ::SYNTHESIZED::s:9Operators8MyNumberV"], @@ -3017,6 +3017,405 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("/CxxOperators/MyClass/operator,", in: tree, asSymbolID: "c:@S@MyClass@F@operator,#&$@S@MyClass#") } + func testMinimalTypeDisambiguation() throws { + enum DeclToken: ExpressibleByStringLiteral { + case text(String) + case internalParameter(String) + case typeIdentifier(String, precise: String) + + init(stringLiteral value: String) { + self = .text(value) + } + } + + func makeFragments(_ tokens: [DeclToken]) -> [SymbolGraph.Symbol.DeclarationFragments.Fragment] { + tokens.map { + switch $0 { + case .text(let spelling): return .init(kind: .text, spelling: spelling, preciseIdentifier: nil) + case .typeIdentifier(let spelling, let precise): return .init(kind: .typeIdentifier, spelling: spelling, preciseIdentifier: precise) + case .internalParameter(let spelling): return .init(kind: .internalParameter, spelling: spelling, preciseIdentifier: nil) + } + } + } + + let optionalType = DeclToken.typeIdentifier("Optional", precise: "s:Sq") + let setType = DeclToken.typeIdentifier("Set", precise: "s:Sh") + let arrayType = DeclToken.typeIdentifier("Array", precise: "s:Sa") + let dictionaryType = DeclToken.typeIdentifier("Dictionary", precise: "s:SD") + + let stringType = DeclToken.typeIdentifier("String", precise: "s:SS") + let intType = DeclToken.typeIdentifier("Int", precise: "s:Si") + let doubleType = DeclToken.typeIdentifier("Double", precise: "s:Sd") + let floatType = DeclToken.typeIdentifier("Float", precise: "s:Sf") + let boolType = DeclToken.typeIdentifier("Bool", precise: "s:Sb") + let voidType = DeclToken.typeIdentifier("Void", precise: "s:s4Voida") + + func makeParameter(_ name: String, decl: [DeclToken]) -> SymbolGraph.Symbol.FunctionSignature.FunctionParameter { + .init(name: name, externalName: nil, declarationFragments: makeFragments([.internalParameter(name), .text("")] + decl), children: []) + } + + func makeSignature(first: DeclToken..., second: DeclToken..., third: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature { + .init( + parameters: [ + makeParameter("first", decl: first), + makeParameter("second", decl: second), + makeParameter("third", decl: third), + ], + returns: makeFragments([voidType]) + ) + } + + // Each overload has one unique parameter + do { + // String [Int] (Double)->Void + // String? [Bool] (Double)->Void + // String? [Int] (Float)->Void + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + // String [Int] (Double)->Void + makeSymbol(id: "function-overload-1", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature( + first: stringType, // String + second: arrayType, "<", intType, ">", // [Int] + third: "(", doubleType, ") -> ", voidType // (Double)->Void + )), + + // String? [Bool] (Double)->Void + makeSymbol(id: "function-overload-2", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature( + first: optionalType, "<", stringType, ">", // String? + second: arrayType, "<", boolType, ">", // [Bool] + third: "(", doubleType, ") -> ", voidType // (Double)->Void + )), + + // String? [Int] (Float)->Void + makeSymbol(id: "function-overload-3", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature( + first: optionalType, "<", stringType, ">", // String? + second: arrayType, "<", intType, ">", // [Int] + third: "(", floatType, ") -> ", voidType // (Float)->Void + )), + ] + )) + ]) + + let (_, context) = try loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(first:second:third:)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-(String,_,_)"), // String _ _ + (symbolID: "function-overload-2", disambiguation: "-(_,[Bool],_)"), // _ [Bool] _ + (symbolID: "function-overload-3", disambiguation: "-(_,_,(Float)->Void)"), // _ _ (Float)->Void + ]) + } + + // Second overload requires combination of two non-unique types to disambiguate + do { + // String Set (Double)->Void + // String? Set (Double)->Void + // String? Set (Float)->Void + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + // String Set (Double)->Void + makeSymbol(id: "function-overload-1", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature( + first: stringType, // String + second: setType, "<", intType, ">", // Set + third: "(", doubleType, ") -> ", voidType // (Double)->Void + )), + + // String? Set (Double)->Void + makeSymbol(id: "function-overload-2", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature( + first: optionalType, "<", stringType, ">", // String? + second: setType, "<", intType, ">", // Set + third: "(", doubleType, ") -> ", voidType // (Double)->Void + )), + + // String? Set (Float)->Void + makeSymbol(id: "function-overload-3", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature( + first: optionalType, "<", stringType, ">", // String? + second: setType, "<", intType, ">", // Set + third: "(", floatType, ") -> ", voidType // (Float)->Void + )), + ] + )) + ]) + + let (_, context) = try loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(first:second:third:)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-(String,_,_)"), // String _ _ + (symbolID: "function-overload-2", disambiguation: "-(String?,_,(Double)->Void)"), // String? _ (Double)->Void + (symbolID: "function-overload-3", disambiguation: "-(_,_,(Float)->Void)"), // _ _ (Float)->Void + ]) + } + + // All overloads require combinations of non-unique types to disambiguate + do { + func makeSignature(first: DeclToken..., second: DeclToken..., third: DeclToken..., fourth: DeclToken..., fifth: DeclToken..., sixth: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature { + .init( + parameters: [ + makeParameter("first", decl: first), + makeParameter("second", decl: second), + makeParameter("third", decl: third), + makeParameter("fourth", decl: fourth), + makeParameter("fifth", decl: fifth), + makeParameter("sixth", decl: sixth), + ], + returns: makeFragments([voidType]) + ) + } + + // String Set [Int] (Double)->Void (Int,Int) [String:Int] + // String? Set [Int] (Double)->Void (Int,Int) [String:Int] + // String? Set [Bool] (Float)->Void (Int,Int) [String:Int] + // String Set [Int] (Double)->Void Bool [Int:String] + // String? Set [Int] (Double)->Void Bool [Int:String] + // String? Set [Bool] (Float)->Void Bool [Int:String] + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + // String Set [Int] (Double)->Void (Int,Int) [String:Int] + makeSymbol(id: "function-overload-1", kind: .func, pathComponents: ["doSomething(first:second:third:fourth:fifth:sixth:)"], signature: makeSignature( + first: stringType, // String + second: setType, "<", intType, ">", // Set + third: arrayType, "<", intType, ">", // [Int] + fourth: "(", doubleType, ") -> ", voidType, // (Double)->Void + fifth: "(", intType, ",", intType, ")", // (Int,Int) + sixth: dictionaryType, "<", stringType, ",", intType, ">" // [String:Int] + )), + + // String? Set [Int] (Double)->Void (Int,Int) [String:Int] + makeSymbol(id: "function-overload-2", kind: .func, pathComponents: ["doSomething(first:second:third:fourth:fifth:sixth:)"], signature: makeSignature( + first: optionalType, "<", stringType, ">", // String? + second: setType, "<", intType, ">", // Set + third: arrayType, "<", intType, ">", // [Int] + fourth: "(", doubleType, ") -> ", voidType, // (Double)->Void + fifth: "(", intType, ",", intType, ")", // (Int,Int) + sixth: dictionaryType, "<", stringType, ",", intType, ">" // [String:Int] + )), + + // String? Set [Bool] (Float)->Void (Int,Int) [String:Int] + makeSymbol(id: "function-overload-3", kind: .func, pathComponents: ["doSomething(first:second:third:fourth:fifth:sixth:)"], signature: makeSignature( + first: optionalType, "<", stringType, ">", // String? + second: setType, "<", intType, ">", // Set + third: arrayType, "<", boolType, ">", // [Bool] + fourth: "(", floatType, ") -> ", voidType, // (Float)->Void + fifth: "(", intType, ",", intType, ")", // (Int,Int) + sixth: dictionaryType, "<", stringType, ",", intType, ">" // [String:Int] + )), + + // String Set [Int] (Double)->Void Bool [Int:String] + makeSymbol(id: "function-overload-4", kind: .func, pathComponents: ["doSomething(first:second:third:fourth:fifth:sixth:)"], signature: makeSignature( + first: stringType, // String + second: setType, "<", intType, ">", // Set + third: arrayType, "<", intType, ">", // [Int] + fourth: "(", doubleType, ") -> ", voidType, // (Double)->Void + fifth: boolType, // Bool + sixth: dictionaryType, "<", intType, ",", stringType, ">" // [Int:String] + )), + + // String? Set [Int] (Double)->Void Bool [Int:String] + makeSymbol(id: "function-overload-5", kind: .func, pathComponents: ["doSomething(first:second:third:fourth:fifth:sixth:)"], signature: makeSignature( + first: optionalType, "<", stringType, ">", // String? + second: setType, "<", intType, ">", // Set + third: arrayType, "<", intType, ">", // [Int] + fourth: "(", doubleType, ") -> ", voidType, // (Double)->Void + fifth: boolType, // Bool + sixth: dictionaryType, "<", intType, ",", stringType, ">" // [Int:String] + )), + + // String? Set [Bool] (Float)->Void Bool [Int:String] + makeSymbol(id: "function-overload-6", kind: .func, pathComponents: ["doSomething(first:second:third:fourth:fifth:sixth:)"], signature: makeSignature( + first: optionalType, "<", stringType, ">", // String? + second: setType, "<", intType, ">", // Set + third: arrayType, "<", boolType, ">", // [Bool] + fourth: "(", floatType, ") -> ", voidType, // (Float)->Void + fifth: boolType, // Bool + sixth: dictionaryType, "<", intType, ",", stringType, ">" // [Int:String] + )), + ] + )) + ]) + + let (_, context) = try loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(first:second:third:fourth:fifth:sixth:)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-(String,_,_,_,(Int,Int),_)"), // String _ _ _ (Int,Int) _ + (symbolID: "function-overload-2", disambiguation: "-(String?,_,[Int],_,(Int,Int),_)"), // String? _ [Int] _ (Int,Int) _ + (symbolID: "function-overload-3", disambiguation: "-(_,_,[Bool],_,(Int,Int),_)"), // _ _ [Bool] _ (Int,Int) _ + (symbolID: "function-overload-4", disambiguation: "-(String,_,_,_,Bool,_)"), // String _ _ _ Bool _ + (symbolID: "function-overload-5", disambiguation: "-(String?,_,[Int],_,Bool,_)"), // String? _ [Int] _ Bool _ + (symbolID: "function-overload-6", disambiguation: "-(_,_,[Bool],_,Bool,_)"), // _ _ [Bool] _ Bool _ + ]) + } + + // Each overload requires a combination parameters and return values to disambiguate + do { + // String Int -> Int + // String Int -> Bool + // String Float -> Int + // String Float -> Bool + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + // String Int -> Int + makeSymbol(id: "function-overload-1", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: .init( + parameters: [ + makeParameter("first", decl: [stringType]), // String + makeParameter("second", decl: [intType]), // Int + ], returns: makeFragments([ // -> + intType // Int + ]) + )), + + // String Int -> Bool + makeSymbol(id: "function-overload-2", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: .init( + parameters: [ + makeParameter("first", decl: [stringType]), // String + makeParameter("second", decl: [intType]), // Int + ], returns: makeFragments([ // -> + boolType // Bool + ]) + )), + + // String Float -> Int + makeSymbol(id: "function-overload-3", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: .init( + parameters: [ + makeParameter("first", decl: [stringType]), // String + makeParameter("second", decl: [floatType]), // Float + ], returns: makeFragments([ // -> + intType // Int + ]) + )), + + // String Float -> Bool + makeSymbol(id: "function-overload-4", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: .init( + parameters: [ + makeParameter("first", decl: [stringType]), // String + makeParameter("second", decl: [floatType]), // Float + ], returns: makeFragments([ // -> + boolType // Bool + ]) + )), + ] + )) + ]) + + let (_, context) = try loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(first:second:)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-(_,Int)->Int"), // ( _ Int ) -> Int + (symbolID: "function-overload-2", disambiguation: "-(_,Int)->Bool"), // ( _ Int ) -> Bool + (symbolID: "function-overload-3", disambiguation: "-(_,Float)->Int"), // ( _ Float ) -> Int + (symbolID: "function-overload-4", disambiguation: "-(_,Float)->Bool"), // ( _ Float ) -> Bool + ]) + } + + // Two overloads with more than 64 parameters, but some unique + do { + let spellOutFormatter = NumberFormatter() + spellOutFormatter.numberStyle = .spellOut + + func makeUniqueToken(_ firstNumber: Int, secondNumber: Int) throws -> DeclToken { + func spelledOut(_ number: Int) throws -> String { + try XCTUnwrap(spellOutFormatter.string(from: .init(value: number))).capitalizingFirstWord() + } + return try .typeIdentifier("Type-\(spelledOut(firstNumber))-\(spelledOut(secondNumber))", precise: "type-\(firstNumber)-\(secondNumber)") + } + + // Each overload has mostly the same 70 parameters, but the even ten parameters are unique. + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: try (1...2).map { symbolNumber in + makeSymbol(id: "function-overload-\(symbolNumber)", kind: .func, pathComponents: ["doSomething(...)"], signature: .init( + parameters: try (1...70).map { parameterNumber in + makeParameter( + "parameter\(parameterNumber)", + decl: parameterNumber.isMultiple(of: 10) + // A unique type name for each overload + ? try [makeUniqueToken(symbolNumber, secondNumber: parameterNumber)] + // The same type name for each overload + : [stringType] + ) + }, returns: makeFragments([ + intType + ]) + )) + } + )) + ]) + + let (_, context) = try loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(...)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-(_,_,_,_,_,_,_,_,_,Type-One-Ten,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_)"), + (symbolID: "function-overload-2", disambiguation: "-(_,_,_,_,_,_,_,_,_,Type-Two-Ten,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_)"), + ]) + } + + // Two overloads the same 5 String parameters falls back to hash disambiguation + do { + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: (1...2).map { symbolNumber in + makeSymbol(id: "function-overload-\(symbolNumber)", kind: .func, pathComponents: ["doSomething(...)"], signature: .init( + // Each overload the same 5 String parameters + parameters: (1...5).map { parameterNumber in + makeParameter("parameter\(parameterNumber)", decl: [stringType]) + }, returns: makeFragments([ + intType + ]) + )) + } + )) + ]) + + let (_, context) = try loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(...)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-3k2fk"), + (symbolID: "function-overload-2", disambiguation: "-3k2fn"), + ]) + } + + // Two overloads the same 70 String parameters falls back to hash disambiguation + do { + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: (1...2).map { symbolNumber in + makeSymbol(id: "function-overload-\(symbolNumber)", kind: .func, pathComponents: ["doSomething(...)"], signature: .init( + // Each overload has the same 70 String parameters. + parameters: (1...70).map { parameterNumber in + makeParameter("parameter\(parameterNumber)", decl: [stringType]) + }, returns: makeFragments([ + intType + ]) + )) + } + )) + ]) + + let (_, context) = try loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(...)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-3k2fk"), + (symbolID: "function-overload-2", disambiguation: "-3k2fn"), + ]) + } + } + func testParsingPaths() { // Check path components without disambiguation assertParsedPathComponents("", []) @@ -3230,9 +3629,18 @@ class PathHierarchyTests: XCTestCase { } catch PathHierarchy.Error.unknownDisambiguation { XCTFail("Symbol for \(path.singleQuoted) not found in tree. Unknown disambiguation.", file: file, line: line) } catch PathHierarchy.Error.lookupCollision(_, _, let collisions) { - let sortedCollisions = collisions.sorted(by: \.disambiguation) - XCTAssertEqual(sortedCollisions.count, expectedCollisions.count, file: file, line: line) - for (actual, expected) in zip(sortedCollisions, expectedCollisions) { + guard collisions.allSatisfy({ $0.node.symbol != nil }) else { + XCTFail("Unexpected non-symbol in collision for symbol link.", file: file, line: line) + return + } + let sortedCollisions = collisions.sorted(by: { lhs, rhs in + if lhs.node.symbol!.identifier.precise == rhs.node.symbol!.identifier.precise { + return lhs.disambiguation < rhs.disambiguation + } + return lhs.node.symbol!.identifier.precise < rhs.node.symbol!.identifier.precise + }) + XCTAssertEqual(sortedCollisions.count, sortedCollisions.count, file: file, line: line) + for (actual, expected) in zip(sortedCollisions, expectedCollisions.sorted(by: \.symbolID)) { XCTAssertEqual(actual.node.symbol?.identifier.precise, expected.symbolID, file: file, line: line) XCTAssertEqual(actual.disambiguation, expected.disambiguation, file: file, line: line) } diff --git a/Tests/SwiftDocCTests/Infrastructure/TinySmallValueIntSetTests.swift b/Tests/SwiftDocCTests/Infrastructure/TinySmallValueIntSetTests.swift new file mode 100644 index 0000000000..8f72b8b467 --- /dev/null +++ b/Tests/SwiftDocCTests/Infrastructure/TinySmallValueIntSetTests.swift @@ -0,0 +1,149 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import XCTest +@testable import SwiftDocC + +class TinySmallValueIntSetTests: XCTestCase { + func testBehavesSameAsSet() { + var tiny = _TinySmallValueIntSet() + var real = Set() + + func AssertEqual(_ lhs: (inserted: Bool, memberAfterInsert: Int), _ rhs: (inserted: Bool, memberAfterInsert: Int), file: StaticString = #filePath, line: UInt = #line) { + XCTAssertEqual(lhs.inserted, rhs.inserted, file: file, line: line) + XCTAssertEqual(lhs.memberAfterInsert, rhs.memberAfterInsert, file: file, line: line) + } + + XCTAssertEqual(tiny.contains(4), real.contains(4)) + AssertEqual(tiny.insert(4), real.insert(4)) + XCTAssertEqual(tiny.contains(4), real.contains(4)) + XCTAssertEqual(tiny.count, real.count) + + AssertEqual(tiny.insert(4), real.insert(4)) + XCTAssertEqual(tiny.contains(4), real.contains(4)) + XCTAssertEqual(tiny.count, real.count) + + AssertEqual(tiny.insert(7), real.insert(7)) + XCTAssertEqual(tiny.contains(7), real.contains(7)) + XCTAssertEqual(tiny.count, real.count) + + XCTAssertEqual(tiny.update(with: 2), real.update(with: 2)) + XCTAssertEqual(tiny.contains(2), real.contains(2)) + XCTAssertEqual(tiny.count, real.count) + + XCTAssertEqual(tiny.remove(9), real.remove(9)) + XCTAssertEqual(tiny.contains(9), real.contains(9)) + XCTAssertEqual(tiny.count, real.count) + + XCTAssertEqual(tiny.remove(4), real.remove(4)) + XCTAssertEqual(tiny.contains(4), real.contains(4)) + XCTAssertEqual(tiny.count, real.count) + + tiny.formUnion([19]) + real.formUnion([19]) + XCTAssertEqual(tiny.contains(19), real.contains(19)) + XCTAssertEqual(tiny.count, real.count) + + tiny.formSymmetricDifference([9]) + real.formSymmetricDifference([9]) + XCTAssertEqual(tiny.contains(7), real.contains(7)) + XCTAssertEqual(tiny.contains(9), real.contains(9)) + XCTAssertEqual(tiny.count, real.count) + + tiny.formIntersection([5,6,7]) + real.formIntersection([5,6,7]) + XCTAssertEqual(tiny.contains(4), real.contains(4)) + XCTAssertEqual(tiny.contains(5), real.contains(5)) + XCTAssertEqual(tiny.contains(6), real.contains(6)) + XCTAssertEqual(tiny.contains(7), real.contains(7)) + XCTAssertEqual(tiny.contains(8), real.contains(8)) + XCTAssertEqual(tiny.contains(9), real.contains(9)) + XCTAssertEqual(tiny.count, real.count) + + tiny.formUnion([11,29]) + real.formUnion([11,29]) + XCTAssertEqual(tiny.contains(11), real.contains(11)) + XCTAssertEqual(tiny.contains(29), real.contains(29)) + XCTAssertEqual(tiny.count, real.count) + + XCTAssertEqual(tiny.isSuperset(of: tiny), real.isSuperset(of: real)) + XCTAssertEqual(tiny.isSuperset(of: []), real.isSuperset(of: [])) + XCTAssertEqual(tiny.isSuperset(of: .init(tiny.dropFirst())), real.isSuperset(of: .init(real.dropFirst()))) + XCTAssertEqual(tiny.isSuperset(of: .init(tiny.dropLast())), real.isSuperset(of: .init(real.dropLast()))) + } + + func testCombinations() { + do { + let tiny: _TinySmallValueIntSet = [0,1,2] + XCTAssertEqual(tiny.combinationsToCheck().map { $0.sorted() }, [ + [0], [1], [2], + [0,1], [0,2], [1,2], + [0,1,2] + ]) + } + + do { + let tiny: _TinySmallValueIntSet = [2,5,9] + XCTAssertEqual(tiny.combinationsToCheck().map { $0.sorted() }, [ + [2], [5], [9], + [2,5], [2,9], [5,9], + [2,5,9] + ]) + } + + do { + let tiny: _TinySmallValueIntSet = [3,4,7,11,15,16] + + let expected: [[Int]] = [ + // 1 elements + [3], [4], [7], [11], [15], [16], + // 2 elements + [3,4], [3,7], [3,11], [3,15], [3,16], + [4,7], [4,11], [4,15], [4,16], + [7,11], [7,15], [7,16], + [11,15], [11,16], + [15,16], + // 3 elements + [3,4,7], [3,4,11], [3,4,15], [3,4,16], [3,7,11], [3,7,15], [3,7,16], [3,11,15], [3,11,16], [3,15,16], + [4,7,11], [4,7,15], [4,7,16], [4,11,15], [4,11,16], [4,15,16], + [7,11,15], [7,11,16], [7,15,16], + [11,15,16], + // 4 elements + [3,4,7,11], [3,4,7,15], [3,4,7,16], [3,4,11,15], [3,4,11,16], [3,4,15,16], [3,7,11,15], [3,7,11,16], [3,7,15,16], [3,11,15,16], + [4,7,11,15], [4,7,11,16], [4,7,15,16], [4,11,15,16], + [7,11,15,16], + // 5 elements + [3,4,7,11,15], [3,4,7,11,16], [3,4,7,15,16], [3,4,11,15,16], [3,7,11,15,16], + [4,7,11,15,16], + // 6 elements + [3,4,7,11,15,16], + ] + let actual = tiny.combinationsToCheck().map { Array($0) } + + XCTAssertEqual(expected.count, actual.count) + + // The order of combinations within a given size doesn't matter. + // It's only important that all combinations of a given size exist and that the sizes are in order. + let expectedBySize = [Int: [[Int]]](grouping: expected, by: \.count).sorted(by: \.key).map(\.value) + let actualBySize = [Int: [[Int]]](grouping: actual, by: \.count).sorted(by: \.key).map(\.value) + + for (expectedForSize, actualForSize) in zip(expectedBySize, actualBySize) { + XCTAssertEqual(expectedForSize.count, actualForSize.count) + + // Comparing [Int] descriptions to allow each same-size combination list to have different orders. + // For example, these two lists of combinations (with the last 2 elements swapped) are considered equivalent: + // [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4] + // [1, 2, 3], [1, 2, 4], [2, 3, 4], [1, 3, 4] + XCTAssertEqual(expectedForSize.map(\.description).sorted(), + actualForSize .map(\.description).sorted()) + } + } + } +}