From c9337114d262e06f63d7d59a4a80faf2ce1456b1 Mon Sep 17 00:00:00 2001 From: Morten Bek Ditlevsen Date: Sat, 10 Feb 2018 19:32:04 +0100 Subject: [PATCH] [WIP] Conditional conformance to Hashable for Optional, Dictionary and Array types. (#14247) * Add conditional Hashable conformance to Optional, Dictionary, Array, ArraySlice and ContiguousArray * Modified hashValue implementations The hashValues are now calculated similar to the automatically synthesized values when conforming to Hashable. This entails using _combineHashValues as values of the collections are iterated - as well as calling _mixInt before returning the hash. * Added FIXMEs as suggested by Max Moiseev * Use checkHashable to check Hashable conformance * Use 2 space indentation * Hashing of Dictionary is now independent of traversal order * Added a test to proof failure of (previous) wrong implementation of Dictionary hashValue. Unfortunately it does not work. * Removed '_mixInt' from 'hashValue' implementation of Optional and Array types based on recommendations from lorentey * Another attempt at detecting bad hashing due to traversal order * Dictionary Hashable validation tests now detect bad hashing due to dependence on traversal order * Removed superfluous initial _mixInt call for Dictionary hashValue implementation. * Add more elements to dictionary in test to increase the number of possible permutations - making it more likely to detect order-dependent hashes * Added Hashable conformance to CollectionOfOne, EmptyCollection and Range types * Fix indirect referral to the only member of CollectionOfOne * Re-added Hashable conformance to Range after merge from master * Change hashValue based on comment from @lorentey * Remove tests for conditional Hashable conformance for Range types. This is left for a followup PR * Added tests for CollectionOfOne and EmptyCollection * Added conditional conformance fo Equatable and Hashable for DictionaryLiteral. Added tests too. * Added conditional Equatable and Hashable conformance to Slice * Use 'elementsEqual' for Slice equality operator * Fixed documentation comment and indentation * Fix DictionaryLiteral equality implementation * Revert "Fix DictionaryLiteral equality implementation" This reverts commit 7fc1510bc3c17add09ecd7ae1444fb38410b2115. * Fix DictionaryLiteral equality implementation * Use equalElements(:by:) to compare DictionaryLiteral elements * Added conditional conformance for Equatable and Hashable to AnyCollection * Revert "Use 'elementsEqual' for Slice equality operator" This reverts commit 0ba2278b968c03d96930dd116c415d4e9a67616d. * Revert "Added conditional Equatable and Hashable conformance to Slice" This reverts commit 84f9934bb44172471f041920abfa00e8c3174563. * Added conditional conformance for Equatable and Hashable for ClosedRange --- stdlib/public/core/Arrays.swift.gyb | 18 +++++++++++ stdlib/public/core/ClosedRange.swift | 13 ++++++++ stdlib/public/core/CollectionOfOne.swift | 21 +++++++++++++ stdlib/public/core/Dictionary.swift | 19 ++++++++++++ stdlib/public/core/EmptyCollection.swift | 7 +++++ .../core/ExistentialCollection.swift.gyb | 29 ++++++++++++++++++ stdlib/public/core/Mirror.swift | 30 +++++++++++++++++++ stdlib/public/core/Optional.swift | 21 +++++++++++++ stdlib/public/core/Range.swift | 13 ++++++++ test/stdlib/DictionaryLiteral.swift | 9 ++++++ test/stdlib/Optional.swift | 13 ++++++++ validation-test/stdlib/Arrays.swift.gyb | 19 ++++++++++++ validation-test/stdlib/Dictionary.swift | 25 ++++++++++++++++ validation-test/stdlib/Lazy.swift.gyb | 19 ++++++++++++ 14 files changed, 256 insertions(+) diff --git a/stdlib/public/core/Arrays.swift.gyb b/stdlib/public/core/Arrays.swift.gyb index ff63bb03717a4..3953203c92a92 100644 --- a/stdlib/public/core/Arrays.swift.gyb +++ b/stdlib/public/core/Arrays.swift.gyb @@ -2297,6 +2297,24 @@ extension ${Self} : Equatable where Element : Equatable { } } +extension ${Self} : Hashable where Element : Hashable { + /// The hash value for the array. + /// + /// Two arrays that are equal will always have equal hash values. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + // FIXME(ABI)#177: Issue applies to Array too + var result: Int = 0 + for element in self { + result = _combineHashValues(result, element.hashValue) + } + return result + } +} + extension ${Self} { /// Calls the given closure with a pointer to the underlying bytes of the /// array's mutable contiguous storage. diff --git a/stdlib/public/core/ClosedRange.swift b/stdlib/public/core/ClosedRange.swift index a8f5c28f727a7..768b89ea2b53b 100644 --- a/stdlib/public/core/ClosedRange.swift +++ b/stdlib/public/core/ClosedRange.swift @@ -381,6 +381,19 @@ extension ClosedRange: Equatable { } } +extension ClosedRange : Hashable where Bound : Hashable { + /// The hash value for the range. + /// + /// Two ranges that are equal will always have equal hash values. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + return _combineHashValues(lowerBound.hashValue, upperBound.hashValue) + } +} + extension ClosedRange : CustomStringConvertible { /// A textual representation of the range. @_inlineable // FIXME(sil-serialize-all)... diff --git a/stdlib/public/core/CollectionOfOne.swift b/stdlib/public/core/CollectionOfOne.swift index ba345fa59fd49..a108980f90ce4 100644 --- a/stdlib/public/core/CollectionOfOne.swift +++ b/stdlib/public/core/CollectionOfOne.swift @@ -136,6 +136,27 @@ extension CollectionOfOne: RandomAccessCollection, MutableCollection { } } +extension CollectionOfOne : Equatable where Element : Equatable { + /// Returns a Boolean value indicating whether two collections are equal. + @_inlineable // FIXME(sil-serialize-all) + public static func == (lhs: CollectionOfOne, rhs: CollectionOfOne) -> Bool { + return lhs._element == rhs._element + } +} + +extension CollectionOfOne : Hashable where Element : Hashable { + /// The hash value for the collection. + /// + /// Two collection that are equal will always have equal hash values. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + return _element.hashValue + } +} + extension CollectionOfOne : CustomDebugStringConvertible { /// A textual representation of `self`, suitable for debugging. @_inlineable // FIXME(sil-serialize-all) diff --git a/stdlib/public/core/Dictionary.swift b/stdlib/public/core/Dictionary.swift index 94a008145802c..44d2397a420b4 100644 --- a/stdlib/public/core/Dictionary.swift +++ b/stdlib/public/core/Dictionary.swift @@ -1446,6 +1446,25 @@ extension Dictionary: Equatable where Value: Equatable { } } +extension Dictionary: Hashable where Value: Hashable { + /// The hash value for the dictionary. + /// + /// Two dictionaries that are equal will always have equal hash values. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + // FIXME(ABI)#177: Issue applies to Dictionary too + var result: Int = 0 + for (k, v) in self { + let combined = _combineHashValues(k.hashValue, v.hashValue) + result ^= _mixInt(combined) + } + return result + } +} + extension Dictionary: CustomStringConvertible, CustomDebugStringConvertible { @_inlineable // FIXME(sil-serialize-all) @_versioned // FIXME(sil-serialize-all) diff --git a/stdlib/public/core/EmptyCollection.swift b/stdlib/public/core/EmptyCollection.swift index 4fb6d8d737aa3..ca85ff26fd009 100644 --- a/stdlib/public/core/EmptyCollection.swift +++ b/stdlib/public/core/EmptyCollection.swift @@ -173,5 +173,12 @@ extension EmptyCollection : Equatable { } } +extension EmptyCollection : Hashable { + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + return 0 + } +} + // @available(*, deprecated, renamed: "EmptyCollection.Iterator") public typealias EmptyIterator = EmptyCollection.Iterator diff --git a/stdlib/public/core/ExistentialCollection.swift.gyb b/stdlib/public/core/ExistentialCollection.swift.gyb index 49fd2b0485464..9c6aae223374d 100644 --- a/stdlib/public/core/ExistentialCollection.swift.gyb +++ b/stdlib/public/core/ExistentialCollection.swift.gyb @@ -1243,3 +1243,32 @@ extension ${Self}: _AnyCollectionProtocol { } } % end + +extension AnyCollection : Equatable where Element : Equatable { + @_inlineable // FIXME(sil-serialize-all) + public static func ==(lhs: AnyCollection, rhs: AnyCollection) -> Bool { + let lhsCount = lhs.count + if lhs.count != rhs.count { + return false + } + return lhs.elementsEqual(rhs) + } +} + +extension AnyCollection : Hashable where Element : Hashable { + /// The hash value for the collection. + /// + /// Two `AnyCollection` values that are equal will always have equal hash values. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + // FIXME(ABI)#177: Issue applies to AnyCollection too + var result: Int = 0 + for element in self { + result = _combineHashValues(result, element.hashValue) + } + return result + } +} diff --git a/stdlib/public/core/Mirror.swift b/stdlib/public/core/Mirror.swift index 08ee521dc97de..c162df30b4dbc 100644 --- a/stdlib/public/core/Mirror.swift +++ b/stdlib/public/core/Mirror.swift @@ -761,6 +761,36 @@ extension DictionaryLiteral : RandomAccessCollection { } } +extension DictionaryLiteral : Equatable where Key: Equatable, Value : Equatable { + @_inlineable // FIXME(sil-serialize-all) + public static func ==(lhs: DictionaryLiteral, rhs: DictionaryLiteral) -> Bool { + if lhs.count != rhs.count { + return false + } + + return lhs.elementsEqual(rhs, by: ==) + } +} + +extension DictionaryLiteral : Hashable where Key: Hashable, Value : Hashable { + /// The hash value for the collection. + /// + /// Two `DictionaryLiteral` values that are equal will always have equal hash values. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + // FIXME(ABI)#177: Issue applies to DictionaryLiteral too + var result: Int = 0 + for element in self { + let elementHashValue = _combineHashValues(element.key.hashValue, element.value.hashValue) + result = _combineHashValues(result, elementHashValue) + } + return result + } +} + extension String { /// Creates a string representing the given value. /// diff --git a/stdlib/public/core/Optional.swift b/stdlib/public/core/Optional.swift index 9a5e1c12ba7c0..e32894c5a8c5e 100644 --- a/stdlib/public/core/Optional.swift +++ b/stdlib/public/core/Optional.swift @@ -409,6 +409,27 @@ extension Optional : Equatable where Wrapped : Equatable { } } +extension Optional : Hashable where Wrapped : Hashable { + /// The hash value for the optional. + /// + /// Two optionals that are equal will always have equal hash values. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + var result: Int + switch self { + case .none: + result = 0 + case .some(let wrapped): + result = 1 + result = _combineHashValues(result, wrapped.hashValue) + } + return result + } +} + // Enable pattern matching against the nil literal, even if the element type // isn't equatable. @_fixed_layout diff --git a/stdlib/public/core/Range.swift b/stdlib/public/core/Range.swift index 7bd6f1b13e1b6..f2fe226650615 100644 --- a/stdlib/public/core/Range.swift +++ b/stdlib/public/core/Range.swift @@ -398,6 +398,19 @@ extension Range: Equatable { } } +extension Range : Hashable where Bound : Hashable { + /// The hash value for the range. + /// + /// Two ranges that are equal will always have equal hash values. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + return _combineHashValues(lowerBound.hashValue, upperBound.hashValue) + } +} + /// A partial half-open interval up to, but not including, an upper bound. /// /// You create `PartialRangeUpTo` instances by using the prefix half-open range diff --git a/test/stdlib/DictionaryLiteral.swift b/test/stdlib/DictionaryLiteral.swift index 8fc3c8875060d..ceb2c0f18a647 100644 --- a/test/stdlib/DictionaryLiteral.swift +++ b/test/stdlib/DictionaryLiteral.swift @@ -54,3 +54,12 @@ expectType(DictionaryLiteral.self, &hetero1) var hetero2: DictionaryLiteral = ["a": 1 as NSNumber, "b": "Foo" as NSString] expectType(DictionaryLiteral.self, &hetero2) + +let instances: [DictionaryLiteral] = [ + [1: "a", 1: "a", 2: "b"], + [1: "a", 2: "b", 1: "a"], + [2: "b", 1: "a", 1: "a"], + [1: "a", 1: "a", 1: "a"] +] +checkEquatable(instances, oracle: { $0 == $1 }) +checkHashable(instances, equalityOracle: { $0 == $1 }) diff --git a/test/stdlib/Optional.swift b/test/stdlib/Optional.swift index 30c4b94270e0d..5b714716695a6 100644 --- a/test/stdlib/Optional.swift +++ b/test/stdlib/Optional.swift @@ -69,6 +69,19 @@ OptionalTests.test("Equatable") { expectEqual([false, true, true, true, true, false], testRelation(!=)) } +OptionalTests.test("Hashable") { + let o1: Optional = .some(1010) + let o2: Optional = .some(2020) + let o3: Optional = .none + checkHashable([o1, o2, o3], equalityOracle: { $0 == $1 }) + + let oo1: Optional> = .some(.some(1010)) + let oo2: Optional> = .some(.some(2010)) + let oo3: Optional> = .some(.none) + let oo4: Optional> = .none + checkHashable([oo1, oo2, oo3, oo4], equalityOracle: { $0 == $1 }) +} + OptionalTests.test("CustomReflectable") { // Test with a non-refcountable type. do { diff --git a/validation-test/stdlib/Arrays.swift.gyb b/validation-test/stdlib/Arrays.swift.gyb index 032b4d969c868..8f810591f8755 100644 --- a/validation-test/stdlib/Arrays.swift.gyb +++ b/validation-test/stdlib/Arrays.swift.gyb @@ -189,6 +189,25 @@ ArrayTestSuite.test("init(repeating:count:)") { } } +ArrayTestSuite.test("Hashable") { + let a1: Array = [1, 2, 3] + let a2: Array = [1, 3, 2] + let a3: Array = [3, 1, 2] + let a4: Array = [1, 2] + let a5: Array = [1] + let a6: Array = [] + let a7: Array = [1, 1, 1] + checkHashable([a1, a2, a3, a4, a5, a6, a7], equalityOracle: { $0 == $1 }) + + let aa1: Array> = [[], [1], [1, 2], [2, 1]] + let aa2: Array> = [[], [1], [2, 1], [2, 1]] + let aa3: Array> = [[1], [], [2, 1], [2, 1]] + let aa4: Array> = [[1], [], [2, 1], [2]] + let aa5: Array> = [[1], [], [2, 1]] + checkHashable([aa1, aa2, aa3, aa4, aa5], equalityOracle: { $0 == $1 }) +} + + #if _runtime(_ObjC) //===----------------------------------------------------------------------===// // NSArray -> Array bridging tests. diff --git a/validation-test/stdlib/Dictionary.swift b/validation-test/stdlib/Dictionary.swift index 4061f913a2dfb..dd93480f58648 100644 --- a/validation-test/stdlib/Dictionary.swift +++ b/validation-test/stdlib/Dictionary.swift @@ -83,6 +83,31 @@ DictionaryTestSuite.test("Index.Hashable") { expectNotNil(e[d.startIndex]) } +DictionaryTestSuite.test("Hashable") { + let d1: Dictionary = [1: "meow", 2: "meow", 3: "meow"] + let d2: Dictionary = [1: "meow", 2: "meow", 3: "mooo"] + let d3: Dictionary = [1: "meow", 2: "meow", 4: "meow"] + let d4: Dictionary = [1: "meow", 2: "meow", 4: "mooo"] + checkHashable([d1, d2, d3, d4], equalityOracle: { $0 == $1 }) + + let dd1: Dictionary> = [1: [2: "meow"]] + let dd2: Dictionary> = [2: [1: "meow"]] + let dd3: Dictionary> = [2: [2: "meow"]] + let dd4: Dictionary> = [1: [1: "meow"]] + let dd5: Dictionary> = [2: [2: "mooo"]] + let dd6: Dictionary> = [2: [:]] + let dd7: Dictionary> = [:] + checkHashable([dd1, dd2, dd3, dd4, dd5, dd6, dd7], equalityOracle: { $0 == $1 }) + + // Check that hash is equal even though dictionary is traversed differently + var d5: Dictionary = [1: "meow", 2: "meow", 3: "mooo", 4: "woof", 5: "baah", 6: "mooo"] + let expected = d5.hashValue + for capacity in [4, 8, 16, 32, 64, 128, 256] { + d5.reserveCapacity(capacity) + expectEqual(d5.hashValue, expected) + } +} + DictionaryTestSuite.test("valueDestruction") { var d1 = Dictionary() for i in 100...110 { diff --git a/validation-test/stdlib/Lazy.swift.gyb b/validation-test/stdlib/Lazy.swift.gyb index 97844c01ae5d6..ac710767ef47b 100644 --- a/validation-test/stdlib/Lazy.swift.gyb +++ b/validation-test/stdlib/Lazy.swift.gyb @@ -209,6 +209,20 @@ LazyTestSuite.test("CollectionOfOne/{CustomDebugStringConvertible,CustomReflecta c) } +LazyTestSuite.test("CollectionOfOne/Equatable") { + let c = CollectionOfOne(42) + let d = CollectionOfOne(43) + let instances = [ c, d ] + checkEquatable(instances, oracle: { $0 == $1 }) +} + +LazyTestSuite.test("CollectionOfOne/Hashable") { + let c = CollectionOfOne(42) + let d = CollectionOfOne(43) + let instances = [ c, d ] + checkHashable(instances, equalityOracle: { $0 == $1 }) +} + //===----------------------------------------------------------------------===// // EmptyCollection //===----------------------------------------------------------------------===// @@ -243,6 +257,11 @@ LazyTestSuite.test("EmptyCollection/Equatable") { checkEquatable(instances, oracle: { $0 == $1 }) } +LazyTestSuite.test("EmptyCollection/Equatable") { + let instances = [ EmptyCollection>() ] + checkHashable(instances, equalityOracle: { $0 == $1 }) +} + LazyTestSuite.test("EmptyCollection/AssociatedTypes") { typealias Subject = EmptyCollection> expectRandomAccessCollectionAssociatedTypes(