Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggest the minimal type disambiguation when an overload doesn't have any unique types #1087

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6109d28
Suggested only the minimal type disambiguation
d-ronnqvist Nov 4, 2024
4d2d482
Support disambiguating using a mix of parameter types and return types
d-ronnqvist Nov 4, 2024
0b629a3
Skip checking columns that are common for all overloads
d-ronnqvist Nov 5, 2024
7684ae0
Use Swift Algorithms package for combinations
d-ronnqvist Nov 5, 2024
d9864ca
Use specialized Set implementation for few overloads and with types
d-ronnqvist Nov 5, 2024
b010bb9
Allow each Int Set to specialize its creation of combinations
d-ronnqvist Nov 5, 2024
bf66819
Avoid mapping combinations for large sizes to Set<Int>
d-ronnqvist Nov 5, 2024
2c2d7ef
Avoid reallocations when generating "tiny int set" combinations
d-ronnqvist Nov 5, 2024
4ce6917
Avoid indexing into a nested array
d-ronnqvist Nov 5, 2024
efa6fcc
Speed up _TinySmallValueIntSet iteration
d-ronnqvist Nov 5, 2024
596cd90
Avoid accessing a Set twice to check if a value exist and remove it
d-ronnqvist Nov 5, 2024
d48ac32
Avoid temporary allocation when creating set of remaining node IDs
d-ronnqvist Nov 5, 2024
b41fff4
Avoid reallocating the collisions list
d-ronnqvist Nov 5, 2024
07dad86
Use a custom `_TinySmallValueIntSet.isSuperset(of:)` implementation
d-ronnqvist Nov 6, 2024
bc5663f
Use `Table<String>` instead of indexing into `[[String]]`
d-ronnqvist Nov 5, 2024
e6f60c8
Avoid recomputing the type name combinations to check
d-ronnqvist Nov 6, 2024
16e15d0
Compare the type name lengths by number of UTF8 code units
d-ronnqvist Nov 6, 2024
b962d1c
Update code comments, variable names, and internal documentation
d-ronnqvist Nov 6, 2024
e29fc0f
Avoid recomputing type name overlap
d-ronnqvist Nov 6, 2024
a2337ad
Merge branch 'main' into suggest-minimal-type-disambiguation
d-ronnqvist Nov 8, 2024
16a3eef
Fix Swift 5.9 compatibility
d-ronnqvist Nov 11, 2024
770694e
Initialize each `Table` element. Linux requires this.
d-ronnqvist Nov 11, 2024
562d502
Address code review feedback:
d-ronnqvist Nov 12, 2024
e6a5d93
Add detailed comment with example about how to find the fewest type n…
d-ronnqvist Nov 12, 2024
ee94641
Merge branch 'main' into suggest-minimal-type-disambiguation
d-ronnqvist Nov 12, 2024
9170fa2
Don't use swift-algorithm as a _local_ dependency in Swift.org CI
d-ronnqvist Nov 12, 2024
639f131
Add additional test for 70 parameter type disambiguation
d-ronnqvist Nov 13, 2024
00da0a1
Add additional test that overloads with all the same parameters fallb…
d-ronnqvist Nov 13, 2024
178ef42
Remove Swift Algorithms dependency.
d-ronnqvist Nov 13, 2024
9c98702
Merge branch 'main' into suggest-minimal-type-disambiguation
d-ronnqvist Nov 13, 2024
06b4a69
Only try mixed type disambiguation when symbol has both parameters an…
d-ronnqvist Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -387,31 +387,37 @@ extension PathHierarchy.DisambiguationContainer {
) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] {
var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = []

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!
let (element, _) = elementAndTypeNamePairs.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))))
collisions.append((value: element.node, disambiguation: makeDisambiguation(.init(repeating: "_", count: numberOfTypeNames))))
remainingIDs.remove(element.node.identifier)
continue
}
guard typesCount > 0 else { continue } // Need at least one return value to disambiguate

for typeIndex in 0..<typesCount {
let grouped = [String: [Element]](grouping: elements, by: { types($0)![typeIndex] })
for (returnType, elements) in grouped where elements.count == 1 {
// Only one element has this return type
let element = elements.first!
guard remainingIDs.contains(element.node.identifier) else { continue } // Don't disambiguate the same element more than once
var disambiguation = [String](repeating: "_", count: typesCount)
disambiguation[typeIndex] = returnType
collisions.append((value: element.node, disambiguation: makeDisambiguation(disambiguation)))
remainingIDs.remove(element.node.identifier)
continue
guard numberOfTypeNames > 0 else {
continue // Need at least one type name to disambiguate (when there are multiple elements without parameters or return values)
}

let disambiguation = minimalSuggestedDisambiguation(listOfOverloadTypeNames: elementAndTypeNamePairs.map(\.typeNames))

for (pair, disambiguation) in zip(elementAndTypeNamePairs, disambiguation) {
guard let disambiguation else {
continue // This element can't be uniquely disambiguated using these types
}
guard remainingIDs.contains(pair.element.node.identifier) else { continue } // Don't disambiguate the same element more than once
collisions.append((value: pair.element.node, disambiguation: makeDisambiguation(disambiguation)))
remainingIDs.remove(pair.element.node.identifier)
}
}
return collisions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
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.
///
/// - Parameter listOfTypeNames: The lists of type-name lists to shrink to the minimal unique combinations of type names
static func minimalSuggestedDisambiguation(listOfOverloadTypeNames: [[String]]) -> [[String]?] {
// The number of types in each list
guard let numberOfTypes = listOfOverloadTypeNames.first?.count, 0 < numberOfTypes else {
return []
d-ronnqvist marked this conversation as resolved.
Show resolved Hide resolved
}

guard listOfOverloadTypeNames.dropFirst().allSatisfy({ $0.count == numberOfTypes }) else {
assertionFailure("Overloads should always have the same number of parameters")
return []
}

// We find the minimal suggested type-signature disambiguation in two steps.
//
// First, we compute which type names occur in which _other_ overloads.
// For example, these type names (left) have these commonalities between each other (right).
//
// String Int Double [ ] [ 12] [ 1 ]
// String? Int Double [ 2] [0 2] [0 ]
// String? Int Float [ 1 ] [01 ] [ ]
//
// Note that each cell doesn't include its own index.

let table: [[Set<Int>]] = listOfOverloadTypeNames.map { typeNames in
typeNames.indexed().map { column, name in
Set(listOfOverloadTypeNames.indices.filter {
$0 != row && listOfOverloadTypeNames[$0][column] == name
})
}
}


// Second, we iterate over each overload's type names to find the shortest disambiguation.
return listOfOverloadTypeNames.indexed().map { row, overload in
var shortestDisambiguationSoFar: (indicesToInclude: Set<Int>, length: Int)?

// For each overload we iterate over the possible parameter combinations with increasing number of elements in each combination.
for typeNamesToInclude in uniqueSortedCombinations(ofNumbersUpTo: numberOfTypes) {
// Stop if we've already found a match with fewer parameters than this
guard typeNamesToInclude.count <= (shortestDisambiguationSoFar?.indicesToInclude.count ?? .max) else {
break
}

let firstTypeNameToInclude = typeNamesToInclude.first! // The generated `typeNamesToInclude` is never empty.
// Compute which other overloads this combinations of type names also could refer to.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the hardest part of the algorithm for a reader to understand. Could you add a comment here that repeats the example from above and uses that to explain what happens next?

i.e. which values does this iterator yield? The integers from the [01 ] set for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a long description of this algorithm in e6a5d93

let overlap = typeNamesToInclude.dropFirst().reduce(into: table[row][firstTypeNameToInclude]) { partialResult, index in
partialResult.formIntersection(table[row][index])
}

guard overlap.isEmpty else {
// This combination of parameters doesn't disambiguate the result
continue
}

// Track the combined length of these type names in case another overload with the same number of type names is shorter.
let length = typeNamesToInclude.reduce(0) { partialResult, index in
partialResult + overload[index].count
}
if length < (shortestDisambiguationSoFar?.length ?? .max) {
shortestDisambiguationSoFar = (Set(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.
// To compute the overload, start with all the type names and replace the unused ones with "_"
var disambiguation = overload
for col in overload.indices where !indicesToInclude.contains(col) {
disambiguation[col] = "_"
}
return disambiguation
}
}
}

/// Returns a sequence of unique combinations up a given upper bounds, with increasing number of elements in each combination.
///
/// For example, when `upperBounds` is `4`, the sequence will first contain all the 1-element combinations:
/// ```
/// [1], [2], [3], [4],
/// ```
/// Next, it will contains all the 2-element combinations (in _some_ inner order):
/// ```
/// [1, 2], [1, 3], [2, 3], [1, 4], [2, 4], [3, 4],
/// ```
/// Next, it will contains all the 3-element combinations (in _some_ inner order):
/// ```
/// [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4],
/// ```
/// Finally, it will contains all the only 4-element combinations:
/// ```
/// [1, 2, 3, 4],
/// ```
private func uniqueSortedCombinations(ofNumbersUpTo upperBounds: Int) -> some Sequence<[Int]> {

/// An iterator that generates sequences of unique number combinations, as described above.
///
/// The iterator works by generating all combinations of a given size and then returning each element from the list.
/// Once the iterator reaches the end of one size, it generates the next size and returns each element from that list.
struct SortedCombinationsIterator: IteratorProtocol {
typealias Element = [Int]

private var upperBounds: Int
private var currentSize = 0
private var current: [Element]
private var currentSlice: ArraySlice<Element>

init(upperBounds: Int) {
self.upperBounds = upperBounds
self.current = (0..<upperBounds).map { [$0] }
self.currentSlice = current[...]
}

mutating func next() -> Element? {
if !currentSlice.isEmpty {
return currentSlice.removeFirst()
}
guard currentSize < upperBounds else {
return nil
}
currentSize += 1
current = combinations(upTo: upperBounds, previous: current)
guard !current.isEmpty else {
return nil
}
currentSlice = current[...]
return currentSlice.removeFirst()
}

func combinations(upTo upperBounds: Int, previous: [Element]) -> [Element] {
precondition(upperBounds > 0)

var result: [Element] = []
result.reserveCapacity(upperBounds * upperBounds-1)
for number in 0 ..< upperBounds {
for var existing in previous where !existing.contains(number) && (existing.last ?? .max) < number {
existing.append(number)

result.append(existing)
}
}
return result
}
}

return IteratorSequence(SortedCombinationsIterator(upperBounds: upperBounds))
}
Loading