Skip to content

Commit

Permalink
Merge pull request #15382 from lorentey/conditional-hashable2
Browse files Browse the repository at this point in the history
[SE-0143][stdlib] Conditionally conform stdlib types to Hashable
  • Loading branch information
lorentey authored Mar 28, 2018
2 parents 31907d4 + bb60059 commit e485c15
Show file tree
Hide file tree
Showing 18 changed files with 297 additions and 26 deletions.
21 changes: 15 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion lib/ClangImporter/ImportType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions stdlib/public/SDK/Foundation/NSArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyHashable>)
}
}

extension NSArray : Sequence {
/// Return an *iterator* over the elements of this *sequence*.
///
Expand Down
8 changes: 8 additions & 0 deletions stdlib/public/SDK/Foundation/NSDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyHashable, AnyHashable>)
}
}

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.
Expand Down
14 changes: 14 additions & 0 deletions stdlib/public/core/Arrays.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 23 additions & 3 deletions stdlib/public/core/ClosedRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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)...
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 {
@_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)
Expand Down
2 changes: 2 additions & 0 deletions stdlib/public/core/DropWhile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
6 changes: 3 additions & 3 deletions stdlib/public/core/Flatten.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
24 changes: 24 additions & 0 deletions stdlib/public/core/Optional.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions stdlib/public/core/PrefixWhile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
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 {
@_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
Expand Down
2 changes: 2 additions & 0 deletions stdlib/public/core/Reverse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
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
84 changes: 84 additions & 0 deletions validation-test/stdlib/AnyHashable.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit e485c15

Please sign in to comment.