Skip to content

Commit

Permalink
[WIP] Conditional conformance to Hashable for Optional, Dictionary an…
Browse files Browse the repository at this point in the history
…d Array types. (swiftlang#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 7fc1510.

* 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 0ba2278.

* Revert "Added conditional Equatable and Hashable conformance to Slice"

This reverts commit 84f9934.

* Added conditional conformance for Equatable and Hashable for ClosedRange
  • Loading branch information
mortenbekditlevsen authored and lorentey committed Mar 23, 2018
1 parent 4dcfae7 commit c933711
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 0 deletions.
18 changes: 18 additions & 0 deletions stdlib/public/core/Arrays.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -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: <rdar://problem/18915294> 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.
Expand Down
13 changes: 13 additions & 0 deletions stdlib/public/core/ClosedRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)...
Expand Down
21 changes: 21 additions & 0 deletions stdlib/public/core/CollectionOfOne.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions stdlib/public/core/Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: <rdar://problem/18915294> 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)
Expand Down
7 changes: 7 additions & 0 deletions stdlib/public/core/EmptyCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = EmptyCollection<T>.Iterator
29 changes: 29 additions & 0 deletions stdlib/public/core/ExistentialCollection.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -1243,3 +1243,32 @@ extension ${Self}: _AnyCollectionProtocol {
}
}
% end

extension AnyCollection : Equatable where Element : Equatable {
@_inlineable // FIXME(sil-serialize-all)
public static func ==(lhs: AnyCollection<Element>, rhs: AnyCollection<Element>) -> 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: <rdar://problem/18915294> Issue applies to AnyCollection too
var result: Int = 0
for element in self {
result = _combineHashValues(result, element.hashValue)
}
return result
}
}
30 changes: 30 additions & 0 deletions stdlib/public/core/Mirror.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Key, Value>, rhs: DictionaryLiteral<Key, Value>) -> 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: <rdar://problem/18915294> 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.
///
Expand Down
21 changes: 21 additions & 0 deletions stdlib/public/core/Optional.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions stdlib/public/core/Range.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions test/stdlib/DictionaryLiteral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,12 @@ expectType(DictionaryLiteral<String, NSObject>.self, &hetero1)

var hetero2: DictionaryLiteral = ["a": 1 as NSNumber, "b": "Foo" as NSString]
expectType(DictionaryLiteral<String, NSObject>.self, &hetero2)

let instances: [DictionaryLiteral<Int, String>] = [
[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 })
13 changes: 13 additions & 0 deletions test/stdlib/Optional.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ OptionalTests.test("Equatable") {
expectEqual([false, true, true, true, true, false], testRelation(!=))
}

OptionalTests.test("Hashable") {
let o1: Optional<Int> = .some(1010)
let o2: Optional<Int> = .some(2020)
let o3: Optional<Int> = .none
checkHashable([o1, o2, o3], equalityOracle: { $0 == $1 })

let oo1: Optional<Optional<Int>> = .some(.some(1010))
let oo2: Optional<Optional<Int>> = .some(.some(2010))
let oo3: Optional<Optional<Int>> = .some(.none)
let oo4: Optional<Optional<Int>> = .none
checkHashable([oo1, oo2, oo3, oo4], equalityOracle: { $0 == $1 })
}

OptionalTests.test("CustomReflectable") {
// Test with a non-refcountable type.
do {
Expand Down
19 changes: 19 additions & 0 deletions validation-test/stdlib/Arrays.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,25 @@ ArrayTestSuite.test("init(repeating:count:)") {
}
}

ArrayTestSuite.test("Hashable") {
let a1: Array<Int> = [1, 2, 3]
let a2: Array<Int> = [1, 3, 2]
let a3: Array<Int> = [3, 1, 2]
let a4: Array<Int> = [1, 2]
let a5: Array<Int> = [1]
let a6: Array<Int> = []
let a7: Array<Int> = [1, 1, 1]
checkHashable([a1, a2, a3, a4, a5, a6, a7], equalityOracle: { $0 == $1 })

let aa1: Array<Array<Int>> = [[], [1], [1, 2], [2, 1]]
let aa2: Array<Array<Int>> = [[], [1], [2, 1], [2, 1]]
let aa3: Array<Array<Int>> = [[1], [], [2, 1], [2, 1]]
let aa4: Array<Array<Int>> = [[1], [], [2, 1], [2]]
let aa5: Array<Array<Int>> = [[1], [], [2, 1]]
checkHashable([aa1, aa2, aa3, aa4, aa5], equalityOracle: { $0 == $1 })
}


#if _runtime(_ObjC)
//===----------------------------------------------------------------------===//
// NSArray -> Array bridging tests.
Expand Down
25 changes: 25 additions & 0 deletions validation-test/stdlib/Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ DictionaryTestSuite.test("Index.Hashable") {
expectNotNil(e[d.startIndex])
}

DictionaryTestSuite.test("Hashable") {
let d1: Dictionary<Int, String> = [1: "meow", 2: "meow", 3: "meow"]
let d2: Dictionary<Int, String> = [1: "meow", 2: "meow", 3: "mooo"]
let d3: Dictionary<Int, String> = [1: "meow", 2: "meow", 4: "meow"]
let d4: Dictionary<Int, String> = [1: "meow", 2: "meow", 4: "mooo"]
checkHashable([d1, d2, d3, d4], equalityOracle: { $0 == $1 })

let dd1: Dictionary<Int, Dictionary<Int, String>> = [1: [2: "meow"]]
let dd2: Dictionary<Int, Dictionary<Int, String>> = [2: [1: "meow"]]
let dd3: Dictionary<Int, Dictionary<Int, String>> = [2: [2: "meow"]]
let dd4: Dictionary<Int, Dictionary<Int, String>> = [1: [1: "meow"]]
let dd5: Dictionary<Int, Dictionary<Int, String>> = [2: [2: "mooo"]]
let dd6: Dictionary<Int, Dictionary<Int, String>> = [2: [:]]
let dd7: Dictionary<Int, Dictionary<Int, String>> = [:]
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<Int, String> = [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<Int, TestValueTy>()
for i in 100...110 {
Expand Down
19 changes: 19 additions & 0 deletions validation-test/stdlib/Lazy.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,20 @@ LazyTestSuite.test("CollectionOfOne/{CustomDebugStringConvertible,CustomReflecta
c)
}

LazyTestSuite.test("CollectionOfOne/Equatable") {
let c = CollectionOfOne<Int>(42)
let d = CollectionOfOne<Int>(43)
let instances = [ c, d ]
checkEquatable(instances, oracle: { $0 == $1 })
}

LazyTestSuite.test("CollectionOfOne/Hashable") {
let c = CollectionOfOne<Int>(42)
let d = CollectionOfOne<Int>(43)
let instances = [ c, d ]
checkHashable(instances, equalityOracle: { $0 == $1 })
}

//===----------------------------------------------------------------------===//
// EmptyCollection
//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -243,6 +257,11 @@ LazyTestSuite.test("EmptyCollection/Equatable") {
checkEquatable(instances, oracle: { $0 == $1 })
}

LazyTestSuite.test("EmptyCollection/Equatable") {
let instances = [ EmptyCollection<OpaqueValue<Int>>() ]
checkHashable(instances, equalityOracle: { $0 == $1 })
}

LazyTestSuite.test("EmptyCollection/AssociatedTypes") {
typealias Subject = EmptyCollection<OpaqueValue<Int>>
expectRandomAccessCollectionAssociatedTypes(
Expand Down

0 comments on commit c933711

Please sign in to comment.