diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e98fc8622a9a..aa2e9fe8e0f26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,15 @@ Swift 5.0 Swift 4.2 --------- +* [SE-0143][] + + The standard library types `Optional`, `Array`, `ArraySlice`, + `ContiguousArray`, `Dictionary`, `DictionaryLiteral`, `Range`, and + `ClosedRange` now conform to the `Hashable` protocol when their element or + bound types (as the case may be) conform to `Hashable`. This makes + synthesized `Hashable` implementations available for types that include stored + properties of these types. + * [SE-0196][] Custom compile-time warnings or error messages can be emitted using the @@ -152,12 +161,12 @@ Swift 4.1 s[keyPath: p] // "H" ``` -* [SE-0143][] The standard library types `Optional`, `Array`, and - `Dictionary` now conform to the `Equatable` protocol when their element types - conform to `Equatable`. This allows the `==` operator to compose (e.g., one - can compare two values of type `[Int : [Int?]?]` with `==`), as well as use - various algorthims defined for `Equatable` element types, such as - `index(of:)`. +* [SE-0143][] The standard library types `Optional`, `Array`, `ArraySlice`, + `ContiguousArray`, and `Dictionary` now conform to the `Equatable` protocol + when their element types conform to `Equatable`. This allows the `==` operator + to compose (e.g., one can compare two values of type `[Int : [Int?]?]` with + `==`), as well as use various algorithms defined for `Equatable` element + types, such as `index(of:)`. * [SE-0157][] is implemented. Associated types can now declare "recursive" constraints, which require that the associated type conform to the enclosing diff --git a/lib/ClangImporter/ImportType.cpp b/lib/ClangImporter/ImportType.cpp index b8f459fa51cce..1cea178bc52ea 100644 --- a/lib/ClangImporter/ImportType.cpp +++ b/lib/ClangImporter/ImportType.cpp @@ -999,7 +999,15 @@ namespace { if (unboundDecl == Impl.SwiftContext.getDictionaryDecl() || unboundDecl == Impl.SwiftContext.getSetDecl()) { auto &keyType = importedTypeArgs[0]; - if (!Impl.matchesHashableBound(keyType)) { + auto keyStructDecl = keyType->getStructOrBoundGenericStruct(); + if (!Impl.matchesHashableBound(keyType) || + // Dictionary and Array conditionally conform to Hashable, + // but the conformance doesn't necessarily apply with the + // imported versions of their type arguments. + // FIXME: Import their non-Hashable type parameters as + // AnyHashable in this context. + keyStructDecl == Impl.SwiftContext.getDictionaryDecl() || + keyStructDecl == Impl.SwiftContext.getArrayDecl()) { if (auto anyHashable = Impl.SwiftContext.getAnyHashableDecl()) keyType = anyHashable->getDeclaredType(); else diff --git a/stdlib/public/SDK/Foundation/NSArray.swift b/stdlib/public/SDK/Foundation/NSArray.swift index adecca6c4bf72..b5825b13f49cb 100644 --- a/stdlib/public/SDK/Foundation/NSArray.swift +++ b/stdlib/public/SDK/Foundation/NSArray.swift @@ -106,6 +106,14 @@ extension Array : _ObjectiveCBridgeable { } } +extension NSArray : _HasCustomAnyHashableRepresentation { + // Must be @nonobjc to avoid infinite recursion during bridging + @nonobjc + public func _toCustomAnyHashable() -> AnyHashable? { + return AnyHashable(self as! Array) + } +} + extension NSArray : Sequence { /// Return an *iterator* over the elements of this *sequence*. /// diff --git a/stdlib/public/SDK/Foundation/NSDictionary.swift b/stdlib/public/SDK/Foundation/NSDictionary.swift index 57eb4cdf40926..eeb3de3d6c94d 100644 --- a/stdlib/public/SDK/Foundation/NSDictionary.swift +++ b/stdlib/public/SDK/Foundation/NSDictionary.swift @@ -138,6 +138,14 @@ extension Dictionary : _ObjectiveCBridgeable { } } +extension NSDictionary : _HasCustomAnyHashableRepresentation { + // Must be @nonobjc to avoid infinite recursion during bridging + @nonobjc + public func _toCustomAnyHashable() -> AnyHashable? { + return AnyHashable(self as! Dictionary) + } +} + extension NSDictionary : Sequence { // FIXME: A class because we can't pass a struct with class fields through an // [objc] interface without prematurely destroying the references. diff --git a/stdlib/public/core/Arrays.swift.gyb b/stdlib/public/core/Arrays.swift.gyb index 327ce20bcd5fd..536739bf04fa7 100644 --- a/stdlib/public/core/Arrays.swift.gyb +++ b/stdlib/public/core/Arrays.swift.gyb @@ -2297,6 +2297,20 @@ extension ${Self} : Equatable where Element : Equatable { } } +extension ${Self}: Hashable where Element: Hashable { + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + return _hashValue(for: self) + } + + @_inlineable // FIXME(sil-serialize-all) + public func _hash(into hasher: inout _Hasher) { + for element in self { + hasher.append(element) + } + } +} + 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 672b7947e3102..8a12b3f214027 100644 --- a/stdlib/public/core/ClosedRange.swift +++ b/stdlib/public/core/ClosedRange.swift @@ -166,14 +166,21 @@ extension ClosedRange.Index : Comparable { } } -extension ClosedRange.Index: Hashable +extension ClosedRange.Index: Hashable where Bound: Strideable, Bound.Stride: SignedInteger, Bound: Hashable { + @_inlineable // FIXME(sil-serialize-all) public var hashValue: Int { + return _hashValue(for: self) + } + + @_inlineable // FIXME(sil-serialize-all) + public func _hash(into hasher: inout _Hasher) { switch self { case .inRange(let value): - return value.hashValue + hasher.append(0 as Int8) + hasher.append(value) case .pastEnd: - return .max + hasher.append(1 as Int8) } } } @@ -381,6 +388,19 @@ extension ClosedRange: Equatable { } } +extension ClosedRange: Hashable where Bound: Hashable { + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + return _hashValue(for: self) + } + + @_inlineable // FIXME(sil-serialize-all) + public func _hash(into hasher: inout _Hasher) { + hasher.append(lowerBound) + hasher.append(upperBound) + } +} + extension ClosedRange : CustomStringConvertible { /// A textual representation of the range. @_inlineable // FIXME(sil-serialize-all)... diff --git a/stdlib/public/core/Dictionary.swift b/stdlib/public/core/Dictionary.swift index 94a008145802c..11cc979de83a5 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 { + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + return _hashValue(for: self) + } + + @_inlineable // FIXME(sil-serialize-all) + public func _hash(into hasher: inout _Hasher) { + var commutativeHash = 0 + for (k, v) in self { + var elementHasher = _Hasher() + elementHasher.append(k) + elementHasher.append(v) + commutativeHash ^= elementHasher.finalize() + } + hasher.append(commutativeHash) + } +} + extension Dictionary: CustomStringConvertible, CustomDebugStringConvertible { @_inlineable // FIXME(sil-serialize-all) @_versioned // FIXME(sil-serialize-all) diff --git a/stdlib/public/core/DropWhile.swift b/stdlib/public/core/DropWhile.swift index 08e8ed87658e0..74432e28cb12e 100644 --- a/stdlib/public/core/DropWhile.swift +++ b/stdlib/public/core/DropWhile.swift @@ -187,10 +187,12 @@ extension LazyDropWhileCollection.Index: Equatable, Comparable { } extension LazyDropWhileCollection.Index: Hashable where Base.Index: Hashable { + @_inlineable // FIXME(sil-serialize-all) public var hashValue: Int { return base.hashValue } + @_inlineable // FIXME(sil-serialize-all) public func _hash(into hasher: inout _Hasher) { hasher.append(base) } diff --git a/stdlib/public/core/Flatten.swift b/stdlib/public/core/Flatten.swift index f94f0aff65e92..fb81fb8f242fd 100644 --- a/stdlib/public/core/Flatten.swift +++ b/stdlib/public/core/Flatten.swift @@ -232,15 +232,15 @@ extension FlattenCollection.Index : Comparable { extension FlattenCollection.Index : Hashable where Base.Index : Hashable, Base.Element.Index : Hashable { + @_inlineable // FIXME(sil-serialize-all) public var hashValue: Int { return _hashValue(for: self) } + @_inlineable // FIXME(sil-serialize-all) public func _hash(into hasher: inout _Hasher) { hasher.append(_outer) - if let inner = _inner { - hasher.append(inner) - } + hasher.append(_inner) } } diff --git a/stdlib/public/core/Optional.swift b/stdlib/public/core/Optional.swift index 9a5e1c12ba7c0..bfa31e09ebcfd 100644 --- a/stdlib/public/core/Optional.swift +++ b/stdlib/public/core/Optional.swift @@ -409,6 +409,30 @@ extension Optional : Equatable where Wrapped : Equatable { } } +extension Optional: Hashable where Wrapped: Hashable { + /// The hash value for the optional instance. + /// + /// 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 { + return _hashValue(for: self) + } + + @_inlineable // FIXME(sil-serialize-all) + public func _hash(into hasher: inout _Hasher) { + switch self { + case .none: + hasher.append(0 as UInt8) + case .some(let wrapped): + hasher.append(1 as UInt8) + hasher.append(wrapped) + } + } +} + // Enable pattern matching against the nil literal, even if the element type // isn't equatable. @_fixed_layout diff --git a/stdlib/public/core/PrefixWhile.swift b/stdlib/public/core/PrefixWhile.swift index 0e07d2faab67f..a7b7de038d9c0 100644 --- a/stdlib/public/core/PrefixWhile.swift +++ b/stdlib/public/core/PrefixWhile.swift @@ -207,10 +207,12 @@ extension LazyPrefixWhileCollection.Index: Comparable { } extension LazyPrefixWhileCollection.Index: Hashable where Base.Index: Hashable { + @_inlineable // FIXME(sil-serialize-all) public var hashValue: Int { return _hashValue(for: self) } + @_inlineable // FIXME(sil-serialize-all) public func _hash(into hasher: inout _Hasher) { switch _value { case .index(let value): diff --git a/stdlib/public/core/Range.swift b/stdlib/public/core/Range.swift index c81f05bbed461..89f6a1d923a96 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 { + @_inlineable // FIXME(sil-serialize-all) + public var hashValue: Int { + return _hashValue(for: self) + } + + @_inlineable // FIXME(sil-serialize-all) + public func _hash(into hasher: inout _Hasher) { + hasher.append(lowerBound) + hasher.append(upperBound) + } +} + /// 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/stdlib/public/core/Reverse.swift b/stdlib/public/core/Reverse.swift index 01585dd3dba90..f826fb35688f4 100644 --- a/stdlib/public/core/Reverse.swift +++ b/stdlib/public/core/Reverse.swift @@ -194,10 +194,12 @@ extension ReversedCollection.Index: Comparable { } extension ReversedCollection.Index: Hashable where Base.Index: Hashable { + @_inlineable // FIXME(sil-serialize-all) public var hashValue: Int { return base.hashValue } + @_inlineable // FIXME(sil-serialize-all) public func _hash(into hasher: inout _Hasher) { hasher.append(base) } 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/AnyHashable.swift.gyb b/validation-test/stdlib/AnyHashable.swift.gyb index 4a5ee24863f57..fcdefb0876b7a 100644 --- a/validation-test/stdlib/AnyHashable.swift.gyb +++ b/validation-test/stdlib/AnyHashable.swift.gyb @@ -767,6 +767,90 @@ AnyHashableTests.test("AnyHashable(Wrappers)/Hashable") { allowBrokenTransitivity: true) } +AnyHashableTests.test("AnyHashable(Set)/Hashable") { + let values: [AnyHashable] = [ + Set([1, 2, 3]), + NSSet(set: [1, 2, 3]), + Set([2, 3, 4]), + NSSet(set: [2, 3, 4]), + Set([Set([1, 2]), Set([3, 4])]), + NSSet(set: [NSSet(set: [1, 2]), NSSet(set: [3, 4])]), + Set([Set([1, 3]), Set([2, 4])]), + NSSet(set: [NSSet(set: [1, 3]), NSSet(set: [2, 4])]), + ] + + func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool { + switch (lhs, rhs) { + case (0...1, 0...1): return true + case (2...3, 2...3): return true + case (4...5, 4...5): return true + case (6...7, 6...7): return true + default: return false + } + } + + checkHashable(values, equalityOracle: equalityOracle, + allowBrokenTransitivity: true) +} + +AnyHashableTests.test("AnyHashable(Array)/Hashable") { + let values: [AnyHashable] = [ + [1, 2, 3], + NSArray(array: [1, 2, 3]), + [3, 2, 1], + NSArray(array: [3, 2, 1]), + [[1, 2], [3, 4]], + NSArray(array: [NSArray(array: [1, 2]), NSArray(array: [3, 4])]), + [[3, 4], [1, 2]], + NSArray(array: [NSArray(array: [3, 4]), NSArray(array: [1, 2])]), + ] + + func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool { + switch (lhs, rhs) { + case (0...1, 0...1): return true + case (2...3, 2...3): return true + case (4...5, 4...5): return true + case (6...7, 6...7): return true + default: return false + } + } + + checkHashable(values, equalityOracle: equalityOracle, + allowBrokenTransitivity: true) +} + +AnyHashableTests.test("AnyHashable(Dictionary)/Hashable") { + let values: [AnyHashable] = [ + ["hello": 1, "world": 2], + NSDictionary(dictionary: ["hello": 1, "world": 2]), + ["hello": 2, "world": 1], + NSDictionary(dictionary: ["hello": 2, "world": 1]), + ["hello": ["foo": 1, "bar": 2], + "world": ["foo": 2, "bar": 1]], + NSDictionary(dictionary: [ + "hello": ["foo": 1, "bar": 2], + "world": ["foo": 2, "bar": 1]]), + ["hello": ["foo": 2, "bar": 1], + "world": ["foo": 1, "bar": 2]], + NSDictionary(dictionary: [ + "hello": ["foo": 2, "bar": 1], + "world": ["foo": 1, "bar": 2]]), + ] + + func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool { + switch (lhs, rhs) { + case (0...1, 0...1): return true + case (2...3, 2...3): return true + case (4...5, 4...5): return true + case (6...7, 6...7): return true + default: return false + } + } + + checkHashable(values, equalityOracle: equalityOracle, + allowBrokenTransitivity: true) +} + AnyHashableTests.test("AnyHashable(_SwiftNativeNSError(MinimalHashablePODSwiftError))/Hashable") { let swiftErrors: [MinimalHashablePODSwiftError] = [ .caseA, .caseA, diff --git a/validation-test/stdlib/AnyHashableDiagnostics.swift b/validation-test/stdlib/AnyHashableDiagnostics.swift deleted file mode 100644 index be5bb5e4615e9..0000000000000 --- a/validation-test/stdlib/AnyHashableDiagnostics.swift +++ /dev/null @@ -1,13 +0,0 @@ -// RUN: %target-typecheck-verify-swift - -// If this test fails, the following types started to conditionally conform to -// `Hashable`. When that happens, please add a custom `AnyHashable` -// representation to corresponding Objective-C types. - -func isHashable(_: T.Type) {} - -isHashable(Int.self) // no-error // Test that `isHashable(_:)` works. - -isHashable(Array.self) // expected-error {{'Array' does not conform to expected type 'Hashable'}} -isHashable(Dictionary.self) // expected-error {{'Dictionary' does not conform to expected type 'Hashable'}} - 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/Dictionary4.swift b/validation-test/stdlib/Dictionary4.swift new file mode 100644 index 0000000000000..aba0cc5977cb0 --- /dev/null +++ b/validation-test/stdlib/Dictionary4.swift @@ -0,0 +1,39 @@ +// RUN: rm -rf %t ; mkdir -p %t +// RUN: %target-build-swift %s -o %t/a.out -swift-version 4 && %target-run %t/a.out + +// REQUIRES: executable_test + +import StdlibUnittest +import StdlibCollectionUnittest + +var DictionaryTestSuite = TestSuite("Dictionary4") + +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) + } +} + +runAllTests()